diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73d69fb2d..148568d76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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? diff --git a/README.md b/README.md index 3f7404305..5b96dbf6c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apiserver/Dockerfile.dev b/apiserver/Dockerfile.dev index d52020735..cb2d1ca28 100644 --- a/apiserver/Dockerfile.dev +++ b/apiserver/Dockerfile.dev @@ -49,5 +49,5 @@ USER captain # Expose container port and run entry point script EXPOSE 8000 -# CMD [ "./bin/takeoff" ] +CMD [ "./bin/takeoff.local" ] diff --git a/apiserver/bin/takeoff.local b/apiserver/bin/takeoff.local new file mode 100755 index 000000000..b89c20874 --- /dev/null +++ b/apiserver/bin/takeoff.local @@ -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 + diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 20798c69e..aa98615fd 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -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, diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 90258259d..cf002af63 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -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", diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 4aa86f6ca..a4f5b194c 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -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( diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 563cc8a40..d790f845d 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -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( diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 55bbfa0d6..bb61e0ada 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -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( diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 4ec06e623..b9221855b 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -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( diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 1bdc48ca3..7039cb875 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -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( diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 58cab3776..4e1e3b39f 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -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 diff --git a/nginx/Dockerfile.dev b/nginx/Dockerfile.dev new file mode 100644 index 000000000..4b90c0dd5 --- /dev/null +++ b/nginx/Dockerfile.dev @@ -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"] diff --git a/nginx/nginx-single-docker-image.conf b/nginx/nginx-single-docker-image.conf index b9f50d664..a087d4e42 100644 --- a/nginx/nginx-single-docker-image.conf +++ b/nginx/nginx-single-docker-image.conf @@ -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; diff --git a/nginx/nginx.conf.dev b/nginx/nginx.conf.dev new file mode 100644 index 000000000..c78893f9f --- /dev/null +++ b/nginx/nginx.conf.dev @@ -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/; + } + } +} diff --git a/setup.sh b/setup.sh index e1fa026b7..a1d9bcbe1 100755 --- a/setup.sh +++ b/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 diff --git a/space/Dockerfile.dev b/space/Dockerfile.dev index d1128a588..862210c33 100644 --- a/space/Dockerfile.dev +++ b/space/Dockerfile.dev @@ -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"] diff --git a/web/Dockerfile.dev b/web/Dockerfile.dev index 29faedef7..5fa751338 100644 --- a/web/Dockerfile.dev +++ b/web/Dockerfile.dev @@ -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"] diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx index 0ce05bd7b..a57d57cbb 100644 --- a/web/components/command-palette/actions/issue-actions/change-state.tsx +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -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 = 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) => { if (!workspaceSlug || !projectId || !issue) return; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 37f8d2626..85b4a8bb9 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -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); diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 1b8878995..59a0e0711 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -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 = 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 = observer((props // : null // ) as { data: IIssue[] | undefined }; - if (!cycle) + if (!activeCycle && isLoading) return ( ); - if (!cycle) + if (!activeCycle) return (
@@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC = observer((props
); - 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) => { 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 = 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 = 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 = observer((props }`} /> - -

{truncateText(cycle.name, 70)}

+ +

{truncateText(activeCycle.name, 70)}

@@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC = observer((props {cycleStatus === "current" ? ( - {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left ) : cycleStatus === "upcoming" ? ( - {findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left ) : cycleStatus === "completed" ? ( - {cycle.total_issues - cycle.completed_issues > 0 && ( + {activeCycle.total_issues - activeCycle.completed_issues > 0 && ( @@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC = observer((props cycleStatus )} - {cycle.is_favorite ? ( + {activeCycle.is_favorite ? (
- +
@@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC = observer((props - Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} + + Pending Issues -{" "} + {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)} +
diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index d6806eaf0..b7acff358 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -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 = 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 = 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 ( <> diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index f020b0998..1349b763b 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -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 = (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) => { 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 = (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 = (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 = (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 (
setUpdateModal(false)} workspaceSlug={workspaceSlug} @@ -152,22 +155,22 @@ export const CyclesBoardCard: FC = (props) => { /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
- - {cycle.name} + + {cycleDetails.name}
@@ -180,7 +183,7 @@ export const CyclesBoardCard: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -196,11 +199,11 @@ export const CyclesBoardCard: FC = (props) => { {issueCount}
- {cycle.assignees.length > 0 && ( - + {cycleDetails.assignees.length > 0 && ( +
- {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -241,7 +244,7 @@ export const CyclesBoardCard: FC = (props) => { )}
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index e69089664..967e8a395 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -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 = 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 ? (
= observer((props) => { : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" } auto-rows-max transition-all `} > - {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
void; handleDeleteCycle?: () => void; handleAddToFavorites?: () => void; @@ -37,52 +33,29 @@ type TCyclesListItem = { }; export const CyclesListItem: FC = (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) => { 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 = (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 = (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 = (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 ( <> setUpdateModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
@@ -181,8 +183,8 @@ export const CyclesListItem: FC = (props) => { - - {cycle.name} + + {cycleDetails.name}
@@ -202,7 +204,7 @@ export const CyclesListItem: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -216,11 +218,11 @@ export const CyclesListItem: FC = (props) => { )} - +
- {cycle.assignees.length > 0 ? ( + {cycleDetails.assignees.length > 0 ? ( - {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -232,7 +234,7 @@ export const CyclesListItem: FC = (props) => {
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 05fa9b92f..686937b71 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -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 = 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 = observer((props) => { return ( <> - {cycles ? ( + {cycleIds ? ( <> - {cycles.length > 0 ? ( + {cycleIds.length > 0 ? (
- {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
= 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 - : filter === "upcoming" - ? cycleStore.projectUpcomingCycles - : cycleStore.projectCycles; + ? projectDraftCycles + : filter === "upcoming" + ? projectUpcomingCycles + : projectAllCycles; return ( <> {layout === "list" && ( <> {cyclesList ? ( - + ) : ( @@ -59,7 +51,7 @@ export const CyclesView: FC = observer((props) => { <> {cyclesList ? ( = observer((props) => { {layout === "gantt" && ( <> {cyclesList ? ( - + ) : ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 33c6254df..dd4eda4a9 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -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,56 +21,51 @@ interface ICycleDelete { export const CycleDeleteModal: React.FC = 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) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle deleted successfully.", - }); - postHogEventTracker("CYCLE_DELETE", { - state: "SUCCESS", - }); - }) - .catch(() => { - postHogEventTracker("CYCLE_DELETE", { - state: "FAILED", - }); + try { + await deleteCycle(workspaceSlug, projectId, cycle.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Cycle deleted successfully.", + }); + postHogEventTracker("CYCLE_DELETE", { + state: "SUCCESS", + }); + }) + .catch(() => { + postHogEventTracker("CYCLE_DELETE", { + state: "FAILED", }); - - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); - - handleClose(); - } catch (error) { - setToastAlert({ - type: "error", - title: "Warning!", - message: "Something went wrong please try again later.", }); - } - else + + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); + + handleClose(); + } catch (error) { setToastAlert({ type: "error", title: "Warning!", message: "Something went wrong please try again later.", }); + } setLoader(false); }; diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 4085b4e34..17338467c 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -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; }; @@ -24,7 +24,7 @@ type Props = { const cycleService = new CycleService(); export const CyclesListGanttChartView: FC = 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 = 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 = 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) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.end_date ?? ""), - })) - : []; + 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 ?? 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 = observer((props) => { getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } blockToRender={(data: ICycle) => } diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 665f9865b..5aa0b0ce4 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -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 = (props) => { const { isOpen, handleClose, data, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); // states const [activeProject, setActiveProject] = useState(projectId); - // toast + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { createCycle, updateCycleDetails } = useCycle(); + // toast alert const { setToastAlert } = useToast(); - const createCycle = async (payload: Partial) => { + const handleCreateCycle = async (payload: Partial) => { 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 = (props) => { }); }; - const updateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial) => { 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 = (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({ diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 4ae5c9d8b..fb1a4bf47 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -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 = 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 = observer((props) => { const submitChanges = (data: Partial) => { 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 = observer((props) => { ); - 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 = 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 = observer((props) => { - {!isCompleted && ( + {!isCompleted && isEditingAllowed && ( { @@ -349,8 +356,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} @@ -373,10 +382,10 @@ export const CycleDetailsSidebar: React.FC = 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} /> @@ -385,8 +394,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { <> {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} @@ -409,10 +420,10 @@ export const CycleDetailsSidebar: React.FC = 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} /> diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index 23f8f8d76..89ecbabba 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -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) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { - title, blockUpdateHandler, blocks, enableReorder, diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 11db81f08..f54a66d15 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -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}`)} > - {truncateText(cycle.name, 40)} +
+ + {truncateText(cycle.name, 40)} +
))} @@ -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} /> @@ -193,20 +203,23 @@ export const CycleIssuesHeader: React.FC = observer(() => { handleDisplayPropertiesUpdate={handleDisplayProperties} /> - + {canUserCreateIssue && ( - + <> + + + )} + {isAuthorizedUser && ( + + )}
diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 4851076f0..c22b9eead 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -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(() => { - + {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}`)} > - {truncateText(module.name, 40)} +
+ + {truncateText(module.name, 40)} +
))}
@@ -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} /> @@ -194,20 +202,23 @@ export const ModuleIssuesHeader: React.FC = observer(() => { handleDisplayPropertiesUpdate={handleDisplayProperties} /> - + {canUserCreateIssue && ( - + <> + + + )} + {canUserCreateIssue && ( - + <> + + + )}
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 166f861d9..a8e1c2c56 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -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}`)} > - {truncateText(view.name, 40)} +
+ + {truncateText(view.name, 40)} +
))} @@ -153,16 +164,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + + m.member)} - states={projectStateStore.states?.[projectId ?? ""] ?? undefined} + states={projectStates} /> @@ -176,18 +188,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { handleDisplayPropertiesUpdate={handleDisplayProperties} /> - { + {canUserCreateIssue && ( - } + )}
); diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 17726e52b..534180aff 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -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 ( <>
@@ -52,18 +60,20 @@ export const ProjectViewsHeader: React.FC = observer(() => {
-
-
- + {canUserCreateIssue && ( +
+
+ +
-
+ )}
); diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 0dd0a3758..4f1b2a1c1 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -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 (
@@ -38,17 +45,18 @@ export const ProjectsHeader = observer(() => { />
)} - - + {isAuthorizedUser && ( + + )}
); diff --git a/web/components/inbox/main-content.tsx b/web/components/inbox/main-content.tsx index 298a33196..159a75161 100644 --- a/web/components/inbox/main-content.tsx +++ b/web/components/inbox/main-content.tsx @@ -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 = { }; 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({ 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) => { @@ -165,16 +163,16 @@ export const InboxMainContent: React.FC = observer(() => { issueStatus === -2 ? "border-yellow-500 bg-yellow-500/10 text-yellow-500" : issueStatus === -1 + ? "border-red-500 bg-red-500/10 text-red-500" + : issueStatus === 0 + ? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? "border-red-500 bg-red-500/10 text-red-500" - : issueStatus === 0 - ? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() - ? "border-red-500 bg-red-500/10 text-red-500" - : "border-gray-500 bg-gray-500/10 text-custom-text-200" - : issueStatus === 1 - ? "border-green-500 bg-green-500/10 text-green-500" - : issueStatus === 2 - ? "border-gray-500 bg-gray-500/10 text-custom-text-200" - : "" + : "border-gray-500 bg-gray-500/10 text-custom-text-200" + : issueStatus === 1 + ? "border-green-500 bg-green-500/10 text-green-500" + : issueStatus === 2 + ? "border-gray-500 bg-gray-500/10 text-custom-text-200" + : "" }`} > {issueStatus === -2 ? ( @@ -225,7 +223,7 @@ export const InboxMainContent: React.FC = observer(() => { ) : null}
-
+
{currentIssueState && ( = 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(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, diff --git a/web/components/inbox/modals/delete-issue-modal.tsx b/web/components/inbox/modals/delete-issue-modal.tsx index 01a3cf643..db0a73f93 100644 --- a/web/components/inbox/modals/delete-issue-modal.tsx +++ b/web/components/inbox/modals/delete-issue-modal.tsx @@ -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 = 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(); diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 3373686ec..b1e455173 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -135,7 +135,7 @@ export const IssueDescriptionForm: FC = (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} diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index ef06e34fa..2aa0d1d44 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -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 = 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 ( <>
@@ -87,7 +89,13 @@ export const CalendarDayTile: React.FC = observer((props) => { {...provided.droppableProps} ref={provided.innerRef} > - + + {enableQuickIssueCreate && !disableIssueCreation && (
= observer((props) => { }} quickAddCallback={quickAddCallback} viewId={viewId} + onOpen={() => setShowAllIssues(true)} />
)} + + {totalIssues > 4 && ( +
+ +
+ )} + {provided.placeholder}
)} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index d2f8e44e8..3e86c4fba 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -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 = 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 = 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]; diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index b92d0e214..05f1a740d 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -26,6 +26,7 @@ type Props = { viewId?: string ) => Promise; viewId?: string; + onOpen?: () => void; }; const defaultValues: Partial = { @@ -56,7 +57,8 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = 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 = observer((props) => { } }; + const handleOpen = () => { + setIsOpen(true); + if (onOpen) onOpen(); + }; + return ( <> {isOpen && ( @@ -165,7 +172,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { } + disabled={!isEditingAllowed} />
diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index 4d244807e..4d65cca09 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -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 = 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 = observer((props) => { ); }; + const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + return ( <> = observer((props) => { icon: , onClick: () => { setTrackElement("MODULE_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }, }} secondaryButton={ @@ -70,10 +81,12 @@ export const ModuleEmptyState: React.FC = observer((props) => { variant="neutral-primary" prependIcon={} onClick={() => setModuleIssuesListModal(true)} + disabled={!isEditingAllowed} > Add an existing issue } + disabled={!isEditingAllowed} />
diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx index d6e863b1d..fca62684c 100644 --- a/web/components/issues/issue-layouts/empty-states/project.tsx +++ b/web/components/issues/issue-layouts/empty-states/project.tsx @@ -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 (
@@ -35,6 +42,7 @@ export const ProjectEmptyState: React.FC = observer(() => { commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); }, }} + disabled={!isEditingAllowed} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 04329ec03..69d1b04d9 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -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 = 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 (
{Object.entries(appliedFilters).map(([key, value]) => { @@ -53,6 +61,7 @@ export const AppliedFiltersList: React.FC = observer((props) => {
{membersFilters.includes(filterKey) && ( handleRemoveFilter(filterKey, val)} members={members} values={value} @@ -63,16 +72,22 @@ export const AppliedFiltersList: React.FC = observer((props) => { )} {filterKey === "labels" && ( handleRemoveFilter("labels", val)} labels={labels} values={value} /> )} {filterKey === "priority" && ( - handleRemoveFilter("priority", val)} values={value} /> + handleRemoveFilter("priority", val)} + values={value} + /> )} {filterKey === "state" && states && ( handleRemoveFilter("state", val)} states={states} values={value} @@ -86,30 +101,35 @@ export const AppliedFiltersList: React.FC = observer((props) => { )} {filterKey === "project" && ( handleRemoveFilter("project", val)} projects={projects} values={value} /> )} - + {isEditingAllowed && ( + + )}
); })} - + {isEditingAllowed && ( + + )}
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx index 9cec9b2f7..08e7aee44 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx @@ -9,10 +9,11 @@ type Props = { handleRemove: (val: string) => void; labels: IIssueLabel[] | undefined; values: string[]; + editable: boolean | undefined; }; export const AppliedLabelsFilters: React.FC = observer((props) => { - const { handleRemove, labels, values } = props; + const { handleRemove, labels, values, editable } = props; return ( <> @@ -30,13 +31,15 @@ export const AppliedLabelsFilters: React.FC = observer((props) => { }} /> {labelDetails.name} - + {editable && ( + + )}
); })} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx index bfa7e9a29..1dd61d339 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -9,10 +9,11 @@ type Props = { handleRemove: (val: string) => void; members: IUserLite[] | undefined; values: string[]; + editable: boolean | undefined; }; export const AppliedMembersFilters: React.FC = observer((props) => { - const { handleRemove, members, values } = props; + const { handleRemove, members, values, editable } = props; return ( <> @@ -25,13 +26,15 @@ export const AppliedMembersFilters: React.FC = observer((props) => {
{memberDetails.display_name} - + {editable && ( + + )}
); })} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index e00d0d829..88b39dc00 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -9,10 +9,11 @@ import { TIssuePriorities } from "types"; type Props = { handleRemove: (val: string) => void; values: string[]; + editable: boolean | undefined; }; export const AppliedPriorityFilters: React.FC = observer((props) => { - const { handleRemove, values } = props; + const { handleRemove, values, editable } = props; return ( <> @@ -20,13 +21,15 @@ export const AppliedPriorityFilters: React.FC = observer((props) => {
{priority} - + {editable && ( + + )}
))} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 018309861..b1e17cfe3 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -10,10 +10,11 @@ type Props = { handleRemove: (val: string) => void; projects: IProject[] | undefined; values: string[]; + editable: boolean | undefined; }; export const AppliedProjectFilters: React.FC = observer((props) => { - const { handleRemove, projects, values } = props; + const { handleRemove, projects, values, editable } = props; return ( <> @@ -34,13 +35,15 @@ export const AppliedProjectFilters: React.FC = observer((props) => { )} {projectDetails.name} - + {editable && ( + + )} ); })} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 8e7592505..9cff84d9b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -10,10 +10,11 @@ type Props = { handleRemove: (val: string) => void; states: IState[]; values: string[]; + editable: boolean | undefined; }; export const AppliedStateFilters: React.FC = observer((props) => { - const { handleRemove, states, values } = props; + const { handleRemove, states, values, editable } = props; return ( <> @@ -26,13 +27,15 @@ export const AppliedStateFilters: React.FC = observer((props) => {
{stateDetails.name} - + {editable && ( + + )}
); })} diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 0c2fa1c7e..9c0ef8511 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -11,10 +11,11 @@ type Props = { children: React.ReactNode; title?: string; placement?: Placement; + disabled?: boolean; }; export const FiltersDropdown: React.FC = (props) => { - const { children, title = "Dropdown", placement } = props; + const { children, title = "Dropdown", placement, disabled = false } = props; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -32,6 +33,7 @@ export const FiltersDropdown: React.FC = (props) => { <> diff --git a/web/components/issues/sidebar-select/label.tsx b/web/components/issues/sidebar-select/label.tsx index b7ef3f48d..3891706f9 100644 --- a/web/components/issues/sidebar-select/label.tsx +++ b/web/components/issues/sidebar-select/label.tsx @@ -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 = observer((props) => { const { setToastAlert } = useToast(); // mobx store const { - projectLabel: { projectLabels, createLabel }, - } = useMobxStore(); + project: { projectLabels, createLabel }, + } = useLabel(); // form info const { handleSubmit, diff --git a/web/components/issues/sidebar-select/parent.tsx b/web/components/issues/sidebar-select/parent.tsx index d7e03989d..cdeb09e90 100644 --- a/web/components/issues/sidebar-select/parent.tsx +++ b/web/components/issues/sidebar-select/parent.tsx @@ -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 = ({ onChange, issueDetails, d issueId={issueId as string} projectId={projectId as string} /> + ); diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index f002b6dda..cc9aa0246 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -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 = 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(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 = 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 = observer((props) => { labelList={issueDetail?.labels ?? []} submitChanges={submitChanges} isNotAllowed={!isAllowed} - uneditable={uneditable ?? false} + uneditable={uneditable || !isAllowed} /> diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index cba7c7aea..7e07670c0 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -78,7 +78,7 @@ export const IssueProperty: React.FC = (props) => { projectId={issue?.project_detail?.id || null} value={issue?.state || null} onChange={(data) => handleStateChange(data)} - disabled={false} + disabled={!editable} hideDropdownArrow /> @@ -89,7 +89,7 @@ export const IssueProperty: React.FC = (props) => { value={issue?.assignees || null} hideDropdownArrow onChange={(val) => handleAssigneeChange(val)} - disabled={false} + disabled={!editable} /> diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index ccce13160..7f924c58b 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -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 = 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; diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index 90dedbc16..fd1e2e73d 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -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 = { export const CreateLabelModal: React.FC = 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 = 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); diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 36b219599..b7b8e6a55 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -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 = { export const CreateUpdateLabelInline = observer( forwardRef(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 = 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 = 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(); diff --git a/web/components/labels/labels-list-modal.tsx b/web/components/labels/labels-list-modal.tsx index b39bfb839..f4bbd9064 100644 --- a/web/components/labels/labels-list-modal.tsx +++ b/web/components/labels/labels-list-modal.tsx @@ -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 = 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( diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx index 05eef6cc6..dbde7f289 100644 --- a/web/components/labels/project-setting-label-item.tsx +++ b/web/components/labels/project-setting-label-item.tsx @@ -1,12 +1,12 @@ 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 +// 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) => { 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, }); }; diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index 75fc40329..72a13c57d 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -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(null); const [isDraggingGroup, setIsDraggingGroup] = useState(false); - // ref + // refs const scrollToRef = useRef(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( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 1fdeb3ce1..01c548371 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -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 ( @@ -92,6 +99,7 @@ export const ModulesListView: React.FC = observer(() => { text: "Build your first module", onClick: () => commandPaletteStore.toggleCreateModuleModal(true), }} + disabled={!isEditingAllowed} /> )} diff --git a/web/components/modules/sidebar-select/select-lead.tsx b/web/components/modules/sidebar-select/select-lead.tsx index 8729b12f4..f675925c4 100644 --- a/web/components/modules/sidebar-select/select-lead.tsx +++ b/web/components/modules/sidebar-select/select-lead.tsx @@ -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) => { - 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) => { ) : (
No lead - + {!disabled && }
) } diff --git a/web/components/modules/sidebar-select/select-members.tsx b/web/components/modules/sidebar-select/select-members.tsx index b1d926d2e..28b173388 100644 --- a/web/components/modules/sidebar-select/select-members.tsx +++ b/web/components/modules/sidebar-select/select-members.tsx @@ -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 = ({ value, onChange }) => { +export const SidebarMembersSelect: React.FC = ({ value, onChange, disabled = false }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -48,6 +49,7 @@ export const SidebarMembersSelect: React.FC = ({ value, onChange }) => {
= ({ value, onChange }) => { ) : (
No members - + {!disabled && }
) } diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 90849e78a..3c8dd0ecf 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -137,7 +137,7 @@ export const ModuleDetailsSidebar: React.FC = 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 = observer((props) => { ); - 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 = observer((props) => { : `${moduleDetails.total_issues}` : `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`; + const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + return ( <> = observer((props) => { - - setModuleDeleteModal(true)}> - - - Delete module - - - + {isEditingAllowed && ( + + setModuleDeleteModal(true)}> + + + Delete module + + + + )}
@@ -304,7 +309,9 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { = observer((props) => { onChange={(value: any) => { submitChanges({ status: value }); }} + disabled={!isEditingAllowed} > {MODULE_STATUS.map((status) => ( @@ -332,7 +340,12 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
- + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} @@ -353,10 +366,10 @@ export const ModuleDetailsSidebar: React.FC = 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} /> @@ -364,7 +377,12 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { <> - + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} @@ -385,10 +403,10 @@ export const ModuleDetailsSidebar: React.FC = 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} /> @@ -410,6 +428,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { name="lead" render={({ field: { value } }) => ( { submitChanges({ lead: val }); @@ -422,6 +441,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { name="members" render={({ field: { value } }) => ( { submitChanges({ members: val }); @@ -546,15 +566,17 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
{currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( <> -
- -
+ {isEditingAllowed && ( +
+ +
+ )} = { first_name: "", diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index a290f48c7..342269ae2 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -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} diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index b4bc5bdd2..217a79bdd 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -31,18 +31,7 @@ export const PagesListView: FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const canUserCreatePage = - currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); - - const emptyStatePrimaryButton = canUserCreatePage - ? { - primaryButton: { - icon: , - text: "Create your first page", - onClick: () => toggleCreatePageModal(true), - }, - } - : {}; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; return ( <> @@ -70,7 +59,12 @@ export const PagesListView: FC = 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: , + text: "Create your first page", + onClick: () => toggleCreatePageModal(true), + }} + disabled={!isEditingAllowed} /> )}
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 8f5f8f2fc..58e692331 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -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 ( @@ -64,6 +71,7 @@ export const RecentPagesList: FC = observer(() => { text: "Create your first page", onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} + disabled={!isEditingAllowed} /> )} diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index f2eb6e4f7..b8c9b953e 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -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 ( @@ -28,7 +35,6 @@ export const ProjectCardList = observer(() => { ); - } return ( <> @@ -65,6 +71,7 @@ export const ProjectCardList = observer(() => { commandPaletteStore.toggleCreateProjectModal(true); }, }} + disabled={!isEditingAllowed} /> )} diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index aa70b4703..dcd00777d 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useProject, useUser } from "hooks/store"; +import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast"; // components import { ConfirmProjectMemberRemove } from "components/project"; @@ -28,14 +28,16 @@ export const ProjectMemberListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store + // store hooks const { - user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject }, projectMember: { removeMemberFromProject, updateMember }, - project: { fetchProjects }, } = useMobxStore(); - // hooks + const { + currentUser, + membership: { currentProjectMemberInfo, currentProjectRole, leaveProject }, + } = useUser(); + const { fetchProjects } = useProject(); + // toast alert const { setToastAlert } = useToast(); // derived values diff --git a/web/components/project/member-list.tsx b/web/components/project/member-list.tsx index 5dbdae83a..5f3aa12c8 100644 --- a/web/components/project/member-list.tsx +++ b/web/components/project/member-list.tsx @@ -2,7 +2,8 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; import { observer } from "mobx-react-lite"; -// mobx store +// hooks +import { useApplication } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; // components import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project"; @@ -12,19 +13,19 @@ import { Button, Loader } from "@plane/ui"; import { Search } from "lucide-react"; export const ProjectMemberList: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - // store - const { - projectMember: { projectMembers, fetchProjectMembers }, - trackEvent: { setTrackElement }, - } = useMobxStore(); - // states const [inviteModal, setInviteModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { + projectMember: { projectMembers, fetchProjectMembers }, + } = useMobxStore(); + const { + eventTracker: { setTrackElement }, + } = useApplication(); const searchedMembers = (projectMembers ?? []).filter((member) => { const fullName = `${member.member.first_name} ${member.member.last_name}`.toLowerCase(); diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 6eb10bcbf..02b76246b 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -4,14 +4,14 @@ import { observer } from "mobx-react-lite"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; -// mobx store +// hooks +import { useApplication, useUser, useWorkspace } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; +import useToast from "hooks/use-toast"; // ui import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; // services import { ProjectMemberService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; // types import { IProjectMember, TUserProjectRole } from "types"; // constants @@ -47,19 +47,23 @@ const projectMemberService = new ProjectMemberService(); export const SendProjectInvitationModal: React.FC = observer((props) => { const { isOpen, members, onClose, onSuccess } = props; - + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - + // toast alert const { setToastAlert } = useToast(); - + // store hooks const { - user: { currentProjectRole }, workspaceMember: { workspaceMembers }, - trackEvent: { postHogEventTracker }, - workspace: { currentWorkspace }, } = useMobxStore(); - + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentWorkspace } = useWorkspace(); + // form info const { formState: { errors, isSubmitting }, reset, diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 61c6333fc..abaddfe15 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -5,7 +5,7 @@ import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks -import { useApplication, useProject } from "hooks/store"; +import { useApplication, useProject, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; @@ -14,6 +14,8 @@ import { copyUrlToClipboard } from "helpers/string.helper"; import { orderArrayBy } from "helpers/array.helper"; // types import { IProject } from "types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectSidebarList: FC = observer(() => { // states @@ -28,6 +30,9 @@ export const ProjectSidebarList: FC = observer(() => { commandPalette: { toggleCreateProjectModal }, eventTracker: { setTrackElement }, } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); // const { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView } = useProject(); // router const router = useRouter(); @@ -36,6 +41,8 @@ export const ProjectSidebarList: FC = observer(() => { // toast const { setToastAlert } = useToast(); + const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const orderedJoinedProjects: IProject[] | undefined = joinedProjects ? orderArrayBy(joinedProjects, "sort_order", "ascending") : undefined; @@ -135,16 +142,18 @@ export const ProjectSidebarList: FC = observer(() => { )} - + {isAuthorizedUser && ( + + )}
)} { )} - + {isAuthorizedUser && ( + + )} )} { - {joinedProjects && joinedProjects.length === 0 && ( + {isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && ( diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 0d1d6802d..f09dc8890 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -56,11 +56,11 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await projectViewsStore .updateView(workspaceSlug, projectId, data?.id as string, payload) .then(() => handleClose()) - .catch(() => + .catch((err) => setToastAlert({ type: "error", title: "Error!", - message: "Something went wrong. Please try again.", + message: err.detail ?? "Something went wrong. Please try again.", }) ); }; diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 95ca50562..2443f7167 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -2,17 +2,22 @@ import React, { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { PencilIcon, StarIcon, TrashIcon } from "lucide-react"; -// mobx store +import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; +import useToast from "hooks/use-toast"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; // ui import { CustomMenu, PhotoFilterIcon } from "@plane/ui"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IProjectView } from "types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { view: IProjectView; @@ -20,14 +25,19 @@ type Props = { export const ProjectViewListItem: React.FC = observer((props) => { const { view } = props; - + // states const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - + // toast alert + const { setToastAlert } = useToast(); + // store hooks const { projectViews: projectViewsStore } = useMobxStore(); + const { + membership: { currentProjectRole }, + } = useUser(); const handleAddToFavorites = () => { if (!workspaceSlug || !projectId) return; @@ -41,8 +51,22 @@ export const ProjectViewListItem: React.FC = observer((props) => { projectViewsStore.removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id); }; + const handleCopyText = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/views/${view.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "View link copied to clipboard.", + }); + }); + }; + const totalFilters = calculateTotalFilters(view.query_data ?? {}); + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + return ( <> {workspaceSlug && projectId && view && ( @@ -73,55 +97,66 @@ export const ProjectViewListItem: React.FC = observer((props) => {

{totalFilters} {totalFilters === 1 ? "filter" : "filters"}

+ {isEditingAllowed && + (view.is_favorite ? ( + + ) : ( + + ))} - {view.is_favorite ? ( - - ) : ( - - )} - { - e.preventDefault(); - e.stopPropagation(); - setCreateUpdateViewModal(true); - }} - > + {isEditingAllowed && ( + <> + { + e.preventDefault(); + e.stopPropagation(); + setCreateUpdateViewModal(true); + }} + > + + + Edit View + + + { + e.preventDefault(); + e.stopPropagation(); + setDeleteViewModal(true); + }} + > + + + Delete View + + + + )} + - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - setDeleteViewModal(true); - }} - > - - - Delete View + + Copy view link diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index e6d9b2544..fca1954e4 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store +import { Plus, Search } from "lucide-react"; +// hooks +import { useApplication, useUser } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; // components import { ProjectViewListItem } from "components/views"; @@ -11,19 +12,28 @@ import { NewEmptyState } from "components/common/new-empty-state"; import { Input, Loader } from "@plane/ui"; // assets import emptyView from "public/empty-state/empty_view.webp"; -// icons -import { Plus, Search } from "lucide-react"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectViewsList = observer(() => { + // states const [query, setQuery] = useState(""); - + // router const router = useRouter(); const { projectId } = router.query; - - const { projectViews: projectViewsStore, commandPalette: commandPaletteStore } = useMobxStore(); + // store hooks + const { projectViews: projectViewsStore } = useMobxStore(); + const { + commandPalette: { toggleCreateViewModal }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + if (!viewsList) return ( @@ -71,8 +81,9 @@ export const ProjectViewsList = observer(() => { primaryButton={{ icon: , text: "Build your first view", - onClick: () => commandPaletteStore.toggleCreateViewModal(true), + onClick: () => toggleCreateViewModal(true), }} + disabled={!isEditingAllowed} /> )} diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index b869d4d63..15ce80919 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { ChevronDown, Dot, XCircle } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useUser } from "hooks/store"; +import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; @@ -35,17 +35,21 @@ type Props = { export const WorkspaceMembersListItem: FC = observer((props) => { const { member } = props; + // states + const [removeMemberModal, setRemoveMemberModal] = useState(false); // router const router = useRouter(); const { workspaceSlug } = router.query; - // store + // store hooks const { workspaceMember: { removeMember, updateMember, updateMemberInvitation, deleteWorkspaceInvitation }, - user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace }, } = useMobxStore(); - // states - const [removeMemberModal, setRemoveMemberModal] = useState(false); - // hooks + const { + currentUser, + currentUserSettings, + membership: { currentWorkspaceMemberInfo, currentWorkspaceRole, leaveWorkspace }, + } = useUser(); + // toast alert const { setToastAlert } = useToast(); const handleLeaveWorkspace = async () => { diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index e5086a9cb..d050bf2fe 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -4,11 +4,13 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; // hooks -import { useApplication } from "hooks/store"; +import { useApplication, useUser } from "hooks/store"; // components import { NotificationPopover } from "components/notifications"; // ui import { Tooltip } from "@plane/ui"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; const workspaceLinks = (workspaceSlug: string) => [ { @@ -36,15 +38,20 @@ const workspaceLinks = (workspaceSlug: string) => [ export const WorkspaceSidebarMenu = observer(() => { // store hooks const { theme: themeStore } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); // router const router = useRouter(); const { workspaceSlug } = router.query; + const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + return (
{workspaceLinks(workspaceSlug as string).map((link, index) => { const isActive = link.name === "Settings" ? router.asPath.includes(link.href) : router.asPath === link.href; - + if (!isAuthorizedUser && link.name === "Analytics") return; return ( diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index beb092781..971fcd111 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; +import { ChevronUp, PenSquare, Search } from "lucide-react"; // hooks -import { useApplication } from "hooks/store"; +import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components import { CreateUpdateDraftIssueModal } from "components/issues"; -// ui -import { ChevronUp, PenSquare, Search } from "lucide-react"; -// constants import { EProjectStore } from "store_legacy/command-palette.store"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; export const WorkspaceSidebarQuickAction = observer(() => { // states @@ -19,11 +19,16 @@ export const WorkspaceSidebarQuickAction = observer(() => { commandPalette: commandPaletteStore, eventTracker: { setTrackElement }, } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); const { storedValue, clearValue } = useLocalStorage("draftedIssue", JSON.stringify({})); const isSidebarCollapsed = themeStore.sidebarCollapsed; + const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + return ( <> { isSidebarCollapsed ? "flex-col gap-1" : "gap-2" }`} > -
- + - {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( - <> -
+ {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( + <> +
- + -
-
- +
+
+ +
-
- - )} -
+ + )} +
+ )}
diff --git a/web/components/workspace/views/default-view-list-item.tsx b/web/components/workspace/views/default-view-list-item.tsx index c290980e1..19b8c0ecf 100644 --- a/web/components/workspace/views/default-view-list-item.tsx +++ b/web/components/workspace/views/default-view-list-item.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { observer } from "mobx-react-lite"; // icons -import { Sparkles } from "lucide-react"; +import { PhotoFilterIcon } from "@plane/ui"; // helpers import { truncateText } from "helpers/string.helper"; @@ -22,7 +22,7 @@ export const GlobalDefaultViewListItem: React.FC = observer((props) => {
- +

{truncateText(view.label, 75)}

diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 697dc3660..9e1e1b39e 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -1,6 +1,11 @@ import { GanttChartSquare, LayoutGrid, List } from "lucide-react"; +// types +import { TCycleLayout, TCycleView } from "types"; -export const CYCLE_TAB_LIST = [ +export const CYCLE_TAB_LIST: { + key: TCycleView; + name: string; +}[] = [ { key: "all", name: "All", @@ -23,7 +28,11 @@ export const CYCLE_TAB_LIST = [ }, ]; -export const CYCLE_VIEW_LAYOUTS = [ +export const CYCLE_VIEW_LAYOUTS: { + key: TCycleLayout; + icon: any; + title: string; +}[] = [ { key: "list", icon: List, diff --git a/web/constants/project.ts b/web/constants/project.ts index f8508ad45..1e9a0213e 100644 --- a/web/constants/project.ts +++ b/web/constants/project.ts @@ -51,6 +51,9 @@ export const PROJECT_AUTOMATION_MONTHS = [ { label: "12 Months", value: 12 }, ]; +export const STATE_GROUP_KEYS = ["backlog", "unstarted", "started", "completed", "cancelled"]; + + export const PROJECT_UNSPLASH_COVERS = [ "https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", "https://images.unsplash.com/photo-1693027407934-e3aa8a54c7ae?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80", diff --git a/web/helpers/state.helper.ts b/web/helpers/state.helper.ts index ef6c3ba77..4c443113a 100644 --- a/web/helpers/state.helper.ts +++ b/web/helpers/state.helper.ts @@ -1,7 +1,19 @@ // types -import { IStateResponse } from "types"; +import { STATE_GROUP_KEYS } from "constants/project"; +import { IState, IStateResponse } from "types"; export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => { if (!unorderedStateGroups) return undefined; return Object.assign({ backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, unorderedStateGroups); }; + +export const sortStates = (states: IState[]) => { + if (!states || states.length === 0) return null; + + return states.sort((stateA, stateB) => { + if (stateA.group === stateB.group) { + return stateA.sequence - stateB.sequence; + } + return STATE_GROUP_KEYS.indexOf(stateA.group) - STATE_GROUP_KEYS.indexOf(stateB.group); + }); +}; diff --git a/web/hooks/store/use-label.ts b/web/hooks/store/use-label.ts index 142caaf05..8069a78e7 100644 --- a/web/hooks/store/use-label.ts +++ b/web/hooks/store/use-label.ts @@ -2,10 +2,10 @@ import { useContext } from "react"; // mobx store import { MobxStoreContext } from "lib/mobx/store-provider"; // types -import { ILabelStore } from "store/label.store"; +import { ILabelRootStore } from "store/label"; -export const useLabel = (): ILabelStore => { +export const useLabel = (): ILabelRootStore => { const context = useContext(MobxStoreContext); if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider"); - return context.label; + return context.labelRoot; }; diff --git a/web/hooks/use-sub-issue.tsx b/web/hooks/use-sub-issue.tsx index a1d2e281c..9da539ee7 100644 --- a/web/hooks/use-sub-issue.tsx +++ b/web/hooks/use-sub-issue.tsx @@ -1,11 +1,11 @@ import { useRouter } from "next/router"; -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; // services import { IssueService } from "services/issue"; // types -import { ISubIssueResponse } from "types"; +import { IIssue, ISubIssueResponse } from "types"; // fetch-keys import { SUB_ISSUES } from "constants/fetch-keys"; @@ -22,9 +22,33 @@ const useSubIssue = (projectId: string, issueId: string, isExpanded: boolean) => shouldFetch ? () => issueService.subIssues(workspaceSlug as string, projectId as string, issueId as string) : null ); + const mutateSubIssues = (issue: IIssue, data: Partial) => { + if (!issue.parent) return; + + mutate( + SUB_ISSUES(issue.parent!), + (prev_data: any) => { + return { + ...prev_data, + sub_issues: prev_data.sub_issues.map((sub_issue: any) => { + if (sub_issue.id === issue.id) { + return { + ...sub_issue, + ...data, + }; + } + return sub_issue; + }), + }; + }, + false + ); + }; + return { subIssues: subIssuesResponse?.sub_issues ?? [], isLoading, + mutateSubIssues, }; }; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index 5f162b302..edc8e4675 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks +import { useApplication, useCycle, useLabel, useModule, useProjectState, useUser } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; // components import { Spinner } from "@plane/ui"; @@ -19,18 +20,24 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store const { - user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, project: { fetchProjectDetails, workspaceProjects }, - projectLabel: { fetchProjectLabels }, projectMember: { fetchProjectMembers }, - projectState: { fetchProjectStates }, projectEstimates: { fetchProjectEstimates }, - cycle: { fetchCycles }, - module: { fetchModules }, projectViews: { fetchAllViews }, inbox: { fetchInboxesList, isInboxEnabled }, - commandPalette: { toggleCreateProjectModal }, } = useMobxStore(); + const { + commandPalette: { toggleCreateProjectModal }, + } = useApplication(); + const { + membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, + } = useUser(); + const { fetchAllCycles } = useCycle(); + const { fetchModules } = useModule(); + const { fetchProjectStates } = useProjectState(); + const { + project: { fetchProjectLabels }, + } = useLabel(); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -68,7 +75,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project cycles useSWR( workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") : null + workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project modules useSWR( @@ -80,7 +87,6 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null ); - // TODO: fetching project pages // fetching project inboxes if inbox is enabled useSWR( workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 251394d61..b73ca3a22 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks -import { useProject, useUser } from "hooks/store"; +import { useLabel, useProject, useUser } from "hooks/store"; import { useMobxStore } from "lib/mobx/store-provider"; // icons import { Button, Spinner } from "@plane/ui"; @@ -17,13 +17,15 @@ export const WorkspaceAuthWrapper: FC = observer((props) const { children } = props; // store hooks const { - workspace: { fetchWorkspaceLabels }, workspaceMember: { fetchWorkspaceMembers, fetchWorkspaceUserProjectsRole }, } = useMobxStore(); const { membership: { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace, fetchUserWorkspaceInfo }, } = useUser(); const { fetchProjects } = useProject(); + const { + workspace: { fetchWorkspaceLabels }, + } = useLabel(); // router const router = useRouter(); const { workspaceSlug } = router.query; diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 2774823dd..60c17d8d4 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useUser } from "hooks/store"; @@ -14,7 +15,8 @@ const AUTHORIZED_ROLES = [20, 15, 10]; export const ProfileAuthWrapper: React.FC = observer((props) => { const { children, className, showProfileIssuesFilter } = props; - // store hooks + const router = useRouter(); + const { membership: { currentWorkspaceRole }, } = useUser(); @@ -23,12 +25,14 @@ export const ProfileAuthWrapper: React.FC = observer((props) => { const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole); + const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); + return (
- {isAuthorized ? ( + {isAuthorized || !isAuthorizedPath ? (
{children}
) : (
diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 6e5757a26..3274ac1bb 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -2,7 +2,7 @@ import React, { Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; // hooks -import { useApplication, useProject } from "hooks/store"; +import { useApplication, useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components @@ -15,6 +15,7 @@ import { Plus } from "lucide-react"; import emptyAnalytics from "public/empty-state/empty_analytics.webp"; // constants import { ANALYTICS_TABS } from "constants/analytics"; +import { EUserWorkspaceRoles } from "constants/workspace"; // type import { NextPageWithLayout } from "types/app"; @@ -24,8 +25,13 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { commandPalette: { toggleCreateProjectModal }, eventTracker: { setTrackElement }, } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); const { workspaceProjects } = useProject(); + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + return ( <> {workspaceProjects && workspaceProjects.length > 0 ? ( @@ -77,6 +83,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { toggleCreateProjectModal(true); }, }} + disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index e54c59ba7..dd1dc3fdc 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,9 +1,8 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useCycle } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -19,24 +18,23 @@ import emptyCycle from "public/empty-state/cycle.svg"; import { NextPageWithLayout } from "types/app"; const CycleDetailPage: NextPageWithLayout = () => { + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - - const { cycle: cycleStore } = useMobxStore(); + // store hooks + const { fetchCycleDetails } = useCycle(); const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const { error } = useSWR( - workspaceSlug && projectId && cycleId ? `CURRENT_CYCLE_DETAILS_${cycleId.toString()}` : null, + workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null, workspaceSlug && projectId && cycleId - ? () => cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) + ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) : null ); - const toggleSidebar = () => { - setValue(`${!isSidebarCollapsed}`); - }; + const toggleSidebar = () => setValue(`${!isSidebarCollapsed}`); return ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 982dbf27d..395d3ae56 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,15 +1,17 @@ -import { Fragment, useCallback, useEffect, useState, ReactElement } from "react"; +import { Fragment, useCallback, useState, ReactElement } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; import { Plus } from "lucide-react"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useCycle, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { NewEmptyState } from "components/common/new-empty-state"; // ui import { Tooltip } from "@plane/ui"; // images @@ -19,61 +21,40 @@ import { TCycleView, TCycleLayout } from "types"; import { NextPageWithLayout } from "types/app"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -// lib cookie -import { setLocalStorage, getLocalStorage } from "lib/local-storage"; -import { NewEmptyState } from "components/common/new-empty-state"; -// TODO: use-local-storage hook instead of lib file. +import { EUserWorkspaceRoles } from "constants/workspace"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); - // store - const { cycle: cycleStore } = useMobxStore(); - const { projectCycles } = cycleStore; + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { projectAllCycles } = useCycle(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; + // local storage + const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( (_layout: TCycleLayout) => { - if (projectId) { - setLocalStorage(`cycle_layout:${projectId}`, _layout); - cycleStore.setCycleLayout(_layout); - } + setCycleLayout(_layout); }, - [cycleStore, projectId] + [setCycleLayout] ); const handleCurrentView = useCallback( (_view: TCycleView) => { - if (projectId) { - setLocalStorage(`cycle_view:${projectId}`, _view); - cycleStore.setCycleView(_view); - if (_view === "draft" && cycleStore.cycleLayout === "gantt") { - handleCurrentLayout("list"); - } - } + setCycleTab(_view); + if (_view === "draft") handleCurrentLayout("list"); }, - [cycleStore, projectId, handleCurrentLayout] + [handleCurrentLayout, setCycleTab] ); - useEffect(() => { - if (projectId) { - const _viewKey = `cycle_view:${projectId}`; - const _viewValue = getLocalStorage(_viewKey); - if (_viewValue && _viewValue !== cycleStore?.cycleView) cycleStore.setCycleView(_viewValue as TCycleView); - else handleCurrentView("all"); + const totalCycles = projectAllCycles?.length ?? 0; - const _layoutKey = `cycle_layout:${projectId}`; - const _layoutValue = getLocalStorage(_layoutKey); - if (_layoutValue && _layoutValue !== cycleStore?.cycleView) - cycleStore.setCycleLayout(_layoutValue as TCycleLayout); - else handleCurrentLayout("list"); - } - }, [projectId, cycleStore, handleCurrentView, handleCurrentLayout]); - - const cycleView = cycleStore?.cycleView; - const cycleLayout = cycleStore?.cycleLayout; - const totalCycles = projectCycles?.length ?? 0; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; if (!workspaceSlug || !projectId) return null; @@ -104,17 +85,16 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { setCreateModal(true); }, }} + disabled={!isEditingAllowed} />
) : ( i.key == cycleStore?.cycleView)} - selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleStore?.cycleView)} - onChange={(i) => { - handleCurrentView(CYCLE_TAB_LIST[i].key as TCycleView); - }} + defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} + selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} + onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} >
@@ -131,26 +111,24 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { ))} - {cycleStore?.cycleView != "active" && ( + {cycleTab !== "active" && (
{CYCLE_VIEW_LAYOUTS.map((layout) => { - if (layout.key === "gantt" && cycleStore?.cycleView === "draft") return null; + if (layout.key === "gantt" && cycleTab === "draft") return null; return ( @@ -163,10 +141,10 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { - {cycleView && cycleLayout && ( + {cycleTab && cycleLayout && ( { - {cycleView && cycleLayout && ( + {cycleTab && cycleLayout && ( { - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleTab && cycleLayout && workspaceSlug && projectId && ( { - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleTab && cycleLayout && workspaceSlug && projectId && ( { - const router = useRouter(); - const { workspaceSlug } = router.query; - const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); return ( diff --git a/web/public/instance-not-ready.svg b/web/public/instance-not-ready.svg deleted file mode 100644 index 393187bdb..000000000 --- a/web/public/instance-not-ready.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/instance-not-ready.webp b/web/public/instance-not-ready.webp new file mode 100644 index 000000000..ceabbcc0d Binary files /dev/null and b/web/public/instance-not-ready.webp differ diff --git a/web/public/onboarding/cycles.svg b/web/public/onboarding/cycles.svg deleted file mode 100644 index 594192b21..000000000 --- a/web/public/onboarding/cycles.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/cycles.webp b/web/public/onboarding/cycles.webp index b76f34652..0fb486a4f 100644 Binary files a/web/public/onboarding/cycles.webp and b/web/public/onboarding/cycles.webp differ diff --git a/web/public/onboarding/issues.svg b/web/public/onboarding/issues.svg deleted file mode 100644 index 04b0018cc..000000000 --- a/web/public/onboarding/issues.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/issues.webp b/web/public/onboarding/issues.webp index e73afda12..52bc34a27 100644 Binary files a/web/public/onboarding/issues.webp and b/web/public/onboarding/issues.webp differ diff --git a/web/public/onboarding/modules.svg b/web/public/onboarding/modules.svg deleted file mode 100644 index 505094f1c..000000000 --- a/web/public/onboarding/modules.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/modules.webp b/web/public/onboarding/modules.webp index b4ceabae1..34eb3445c 100644 Binary files a/web/public/onboarding/modules.webp and b/web/public/onboarding/modules.webp differ diff --git a/web/public/onboarding/onboarding-issues.svg b/web/public/onboarding/onboarding-issues.svg deleted file mode 100644 index c38fc3a32..000000000 --- a/web/public/onboarding/onboarding-issues.svg +++ /dev/null @@ -1,592 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/onboarding-issues.webp b/web/public/onboarding/onboarding-issues.webp new file mode 100644 index 000000000..8ea6318f2 Binary files /dev/null and b/web/public/onboarding/onboarding-issues.webp differ diff --git a/web/public/onboarding/pages.svg b/web/public/onboarding/pages.svg deleted file mode 100644 index a7141522e..000000000 --- a/web/public/onboarding/pages.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/pages.webp b/web/public/onboarding/pages.webp index 90084952b..849925889 100644 Binary files a/web/public/onboarding/pages.webp and b/web/public/onboarding/pages.webp differ diff --git a/web/public/onboarding/sign-in.svg b/web/public/onboarding/sign-in.svg deleted file mode 100644 index 243721412..000000000 --- a/web/public/onboarding/sign-in.svg +++ /dev/null @@ -1,649 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/sign-in.webp b/web/public/onboarding/sign-in.webp new file mode 100644 index 000000000..0c7fb571c Binary files /dev/null and b/web/public/onboarding/sign-in.webp differ diff --git a/web/public/onboarding/views.svg b/web/public/onboarding/views.svg deleted file mode 100644 index 0736ebc12..000000000 --- a/web/public/onboarding/views.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/views.webp b/web/public/onboarding/views.webp index d92a15114..6c8959da2 100644 Binary files a/web/public/onboarding/views.webp and b/web/public/onboarding/views.webp differ diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index e32a49678..355778cb9 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -11,7 +11,7 @@ export class CycleService extends APIService { super(API_BASE_URL); } - async createCycle(workspaceSlug: string, projectId: string, data: any): Promise { + async createCycle(workspaceSlug: string, projectId: string, data: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) .then((response) => response?.data) .catch((error) => { @@ -22,8 +22,8 @@ export class CycleService extends APIService { async getCyclesWithParams( workspaceSlug: string, projectId: string, - cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ): Promise { + cycleType?: "current" + ): Promise> { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { params: { cycle_view: cycleType, diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index 695789998..03f11f48a 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -1,7 +1,8 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import set from "lodash/set"; +import { set, omit } from "lodash"; +import { isFuture, isPast } from "date-fns"; // types -import { ICycle, TCycleView, CycleDateCheckData } from "types"; +import { ICycle, CycleDateCheckData } from "types"; // mobx import { RootStore } from "store/root.store"; // services @@ -10,40 +11,30 @@ import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; export interface ICycleStore { + // states loader: boolean; error: any | null; - - cycleView: TCycleView; - - cycleId: string | null; + // observables cycleMap: { - [projectId: string]: { - [cycleId: string]: ICycle; - }; + [cycleId: string]: ICycle; }; - cycles: { - [projectId: string]: { - [filterType: string]: string[]; - }; + activeCycleMap: { + [cycleId: string]: ICycle; }; - // computed - getCycleById: (cycleId: string) => ICycle | null; - projectCycles: string[] | null; + projectAllCycles: string[] | null; projectCompletedCycles: string[] | null; projectUpcomingCycles: string[] | null; projectDraftCycles: string[] | null; - + projectActiveCycle: string | null; + // computed actions + getCycleById: (cycleId: string) => ICycle | null; + getActiveCycleById: (cycleId: string) => ICycle | null; // actions validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; - - fetchCycles: ( - workspaceSlug: string, - projectId: string, - params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ) => Promise; + fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise>; + fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise>; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateCycleDetails: ( workspaceSlug: string, @@ -52,29 +43,19 @@ export interface ICycleStore { data: Partial ) => Promise; deleteCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; } export class CycleStore implements ICycleStore { + // states loader: boolean = false; error: any | null = null; - - cycleView: TCycleView = "all"; - - cycleId: string | null = null; + // observables cycleMap: { - [projectId: string]: { - [cycleId: string]: ICycle; - }; + [cycleId: string]: ICycle; } = {}; - cycles: { - [projectId: string]: { - [filterType: string]: string[]; - }; - } = {}; - + activeCycleMap: { [cycleId: string]: ICycle } = {}; // root store rootStore; // services @@ -84,29 +65,28 @@ export class CycleStore implements ICycleStore { constructor(_rootStore: RootStore) { makeObservable(this, { - loader: observable, + // states + loader: observable.ref, error: observable.ref, - - cycleId: observable.ref, + // observables cycleMap: observable, - cycles: observable, - + activeCycleMap: observable, // computed - projectCycles: computed, + projectAllCycles: computed, projectCompletedCycles: computed, projectUpcomingCycles: computed, projectDraftCycles: computed, - - // actions + projectActiveCycle: computed, + // computed actions getCycleById: action, - - fetchCycles: action, + getActiveCycleById: action, + // actions + fetchAllCycles: action, + fetchActiveCycle: action, fetchCycleDetails: action, - createCycle: action, updateCycleDetails: action, deleteCycle: action, - addCycleToFavorites: action, removeCycleFromFavorites: action, }); @@ -118,46 +98,86 @@ export class CycleStore implements ICycleStore { } // computed - get projectCycles() { + get projectAllCycles() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; - return this.cycles[projectId]?.all || null; + + const allCycles = Object.keys(this.cycleMap ?? {}).filter( + (cycleId) => this.cycleMap?.[cycleId]?.project === projectId + ); + + return allCycles || null; } get projectCompletedCycles() { - const projectId = this.rootStore.app.router.projectId; + const allCycles = this.projectAllCycles; - if (!projectId) return null; + if (!allCycles) return null; - return this.cycles[projectId]?.completed || null; + const completedCycles = allCycles.filter((cycleId) => { + const hasEndDatePassed = isPast(new Date(this.cycleMap?.[cycleId]?.end_date ?? "")); + + return hasEndDatePassed; + }); + + return completedCycles || null; } get projectUpcomingCycles() { - const projectId = this.rootStore.app.router.projectId; + const allCycles = this.projectAllCycles; - if (!projectId) return null; + if (!allCycles) return null; - return this.cycles[projectId]?.upcoming || null; + const upcomingCycles = allCycles.filter((cycleId) => { + const isStartDateUpcoming = isFuture(new Date(this.cycleMap?.[cycleId]?.start_date ?? "")); + + return isStartDateUpcoming; + }); + + return upcomingCycles || null; } get projectDraftCycles() { - const projectId = this.rootStore.app.router.projectId; + const allCycles = this.projectAllCycles; - if (!projectId) return null; + if (!allCycles) return null; - return this.cycles[projectId]?.draft || null; + const draftCycles = allCycles.filter((cycleId) => { + const cycleDetails = this.cycleMap?.[cycleId]; + + return !cycleDetails?.start_date && !cycleDetails?.end_date; + }); + + return draftCycles || null; } - getCycleById = (cycleId: string) => { + get projectActiveCycle() { const projectId = this.rootStore.app.router.projectId; if (!projectId) return null; - return this.cycleMap?.[projectId]?.[cycleId] || null; - }; + const activeCycle = Object.keys(this.activeCycleMap ?? {}).find( + (cycleId) => this.activeCycleMap?.[cycleId]?.project === projectId + ); + + return activeCycle || null; + } + + /** + * @description returns cycle details by cycle id + * @param cycleId + * @returns + */ + getCycleById = (cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null; + + /** + * @description returns active cycle details by cycle id + * @param cycleId + * @returns + */ + getActiveCycleById = (cycleId: string): ICycle | null => this.activeCycleMap?.[cycleId] ?? null; - // actions validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { try { const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); @@ -168,27 +188,52 @@ export class CycleStore implements ICycleStore { } }; - fetchCycles = async ( - workspaceSlug: string, - projectId: string, - params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" - ) => { + fetchAllCycles = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; this.error = null; - const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId); runInAction(() => { - set(this.cycleMap, [projectId], cyclesResponse); - set(this.cycles, [projectId, params], Object.keys(cyclesResponse)); + Object.values(cyclesResponse).forEach((cycle) => { + set(this.cycleMap, [cycle.id], cycle); + }); this.loader = false; this.error = null; }); + + return cyclesResponse; } catch (error) { console.error("Failed to fetch project cycles in project store", error); this.loader = false; this.error = error; + + throw error; + } + }; + + fetchActiveCycle = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, "current"); + + runInAction(() => { + Object.values(cyclesResponse).forEach((cycle) => { + set(this.activeCycleMap, [cycle.id], cycle); + }); + this.loader = false; + this.error = null; + }); + + return cyclesResponse; + } catch (error) { + this.loader = false; + this.error = error; + + throw error; } }; @@ -197,7 +242,8 @@ export class CycleStore implements ICycleStore { const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); runInAction(() => { - set(this.cycleMap, [projectId, response?.id], response); + set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response }); + set(this.activeCycleMap, [response.id], { ...this.activeCycleMap?.[response.id], ...response }); }); return response; @@ -212,12 +258,10 @@ export class CycleStore implements ICycleStore { const response = await this.cycleService.createCycle(workspaceSlug, projectId, data); runInAction(() => { - set(this.cycleMap, [projectId, response?.id], response); + set(this.cycleMap, [response.id], response); + set(this.activeCycleMap, [response.id], response); }); - const _currentView = this.cycleView === "active" ? "current" : this.cycleView; - this.fetchCycles(workspaceSlug, projectId, _currentView); - return response; } catch (error) { console.log("Failed to create cycle from cycle store"); @@ -227,18 +271,14 @@ export class CycleStore implements ICycleStore { updateCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => { try { - const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); - - const currentCycle = this.cycleMap[projectId][cycleId]; + const response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data); runInAction(() => { - set(this.cycleMap, [projectId, cycleId], { ...currentCycle, ...data }); + set(this.cycleMap, [cycleId], { ...this.cycleMap?.[cycleId], ...data }); + set(this.activeCycleMap, [cycleId], { ...this.activeCycleMap?.[cycleId], ...data }); }); - const _currentView = this.cycleView === "active" ? "current" : this.cycleView; - this.fetchCycles(workspaceSlug, projectId, _currentView); - - return _response; + return response; } catch (error) { console.log("Failed to patch cycle from cycle store"); throw error; @@ -246,32 +286,36 @@ export class CycleStore implements ICycleStore { }; deleteCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { - try { - if (!this.cycleMap?.[projectId]?.[cycleId]) return; + const originalCycle = this.cycleMap[cycleId]; + const originalActiveCycle = this.activeCycleMap[cycleId]; + try { runInAction(() => { - delete this.cycleMap[projectId][cycleId]; + omit(this.cycleMap, [cycleId]); + omit(this.activeCycleMap, [cycleId]); }); - const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); - - return _response; + await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); } catch (error) { console.log("Failed to delete cycle from cycle store"); - const _currentView = this.cycleView === "active" ? "current" : this.cycleView; - this.fetchCycles(workspaceSlug, projectId, _currentView); + runInAction(() => { + set(this.cycleMap, [cycleId], originalCycle); + set(this.activeCycleMap, [cycleId], originalActiveCycle); + }); + throw error; } }; addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { - const currentCycle = this.cycleMap[projectId][cycleId]; - if (currentCycle.is_favorite) return; + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); runInAction(() => { - set(this.cycleMap, [projectId, cycleId, "is_favorite"], true); + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); }); // updating through api. @@ -279,10 +323,12 @@ export class CycleStore implements ICycleStore { return response; } catch (error) { - console.log("Failed to add cycle to favorites in the cycles store", error); + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); runInAction(() => { - set(this.cycleMap, [projectId, cycleId, "is_favorite"], false); + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); }); throw error; @@ -291,22 +337,24 @@ export class CycleStore implements ICycleStore { removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { - const currentCycle = this.cycleMap[projectId][cycleId]; - - if (!currentCycle.is_favorite) return; + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); runInAction(() => { - set(this.cycleMap, [projectId, cycleId, "is_favorite"], false); + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], false); }); const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); return response; } catch (error) { - console.log("Failed to remove cycle from favorites - Cycle Store", error); + const currentCycle = this.getCycleById(cycleId); + const currentActiveCycle = this.getActiveCycleById(cycleId); runInAction(() => { - set(this.cycleMap, [projectId, cycleId, "is_favorite"], true); + if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); + if (currentActiveCycle) set(this.activeCycleMap, [cycleId, "is_favorite"], true); }); throw error; diff --git a/web/store/label/index.ts b/web/store/label/index.ts new file mode 100644 index 000000000..350667e6b --- /dev/null +++ b/web/store/label/index.ts @@ -0,0 +1,47 @@ +import { computed, observable, makeObservable } from "mobx"; +import { RootStore } from "../root.store"; +// types +import { IIssueLabel } from "types"; +import { IProjectLabelStore, ProjectLabelStore } from "./project-label.store"; +import { IWorkspaceLabelStore, WorkspaceLabelStore } from "./workspace-label.store"; + +export interface ILabelRootStore { + // observables + labelMap: Record; + // computed actions + getLabelById: (labelId: string) => IIssueLabel | null; + // sub-stores + project: IProjectLabelStore; + workspace: IWorkspaceLabelStore; +} + +export class LabelRootStore implements ILabelRootStore { + // observables + labelMap: Record = {}; + // root store + rootStore; + // sub-stores + project: IProjectLabelStore; + workspace: IWorkspaceLabelStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + labelMap: observable, + // computed actions + getLabelById: computed, + }); + + // root store + this.rootStore = _rootStore; + // sub-stores + this.project = new ProjectLabelStore(_rootStore); + this.workspace = new WorkspaceLabelStore(_rootStore); + } + + /** + * get label info from the map of labels in the store using label id + * @param labelId + */ + getLabelById = (labelId: string): IIssueLabel | null => this.labelMap?.[labelId] || null; +} diff --git a/web/store/label/project-label.store.ts b/web/store/label/project-label.store.ts new file mode 100644 index 000000000..bfdfab001 --- /dev/null +++ b/web/store/label/project-label.store.ts @@ -0,0 +1,231 @@ +import { action, computed, makeObservable, runInAction } from "mobx"; +import { set } from "lodash"; +// services +import { IssueLabelService } from "services/issue"; +// helpers +import { buildTree } from "helpers/array.helper"; +// types +import { RootStore } from "store/root.store"; +import { IIssueLabel } from "types"; + +export interface IProjectLabelStore { + // computed + projectLabels: IIssueLabel[] | undefined; + projectLabelsTree: IIssueLabel[] | undefined; + // actions + fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise; + createLabel: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateLabel: ( + workspaceSlug: string, + projectId: string, + labelId: string, + data: Partial + ) => Promise; + updateLabelPosition: ( + workspaceSlug: string, + projectId: string, + labelId: string, + parentId: string | null | undefined, + index: number, + isSameParent: boolean, + prevIndex: number | undefined + ) => Promise; + deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise; +} + +export class ProjectLabelStore implements IProjectLabelStore { + // root store + rootStore; + // root store labelMap + labelMap: Record = {}; + // services + issueLabelService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // computed + projectLabels: computed, + projectLabelsTree: computed, + // actions + fetchProjectLabels: action, + createLabel: action, + updateLabel: action, + updateLabelPosition: action, + deleteLabel: action, + }); + + // root store + this.rootStore = _rootStore; + this.labelMap = this.rootStore.labelRoot.labelMap; + // services + this.issueLabelService = new IssueLabelService(); + } + + /** + * Returns the labelMap belongs to a specific project + */ + get projectLabels() { + const projectId = this.rootStore.app.router.query?.projectId; + if (!projectId) return; + return Object.values(this.labelMap).filter((label) => label.project === projectId); + } + + /** + * Returns the labelMap in a tree format + */ + get projectLabelsTree() { + if (!this.projectLabels) return; + return buildTree(this.projectLabels); + } + + /** + * Fetches all the labelMap belongs to a specific project + * @param workspaceSlug + * @param projectId + * @returns Promise + */ + fetchProjectLabels = async (workspaceSlug: string, projectId: string) => { + const response = await this.issueLabelService.getProjectIssueLabels(workspaceSlug, projectId); + runInAction(() => { + response.forEach((label) => { + set(this.labelMap, [label.id], label); + }); + }); + return response; + }; + + /** + * Creates a new label for a specific project and add it to the store + * @param workspaceSlug + * @param projectId + * @param data + * @returns Promise + */ + createLabel = async (workspaceSlug: string, projectId: string, data: Partial) => { + const response = await this.issueLabelService.createIssueLabel(workspaceSlug, projectId, data); + + runInAction(() => { + set(this.labelMap, [response.id], response); + }); + return response; + }; + + /** + * Updates a label for a specific project and update it in the store + * @param workspaceSlug + * @param projectId + * @param labelId + * @param data + * @returns Promise + */ + updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial) => { + const originalLabel = this.labelMap[labelId]; + try { + runInAction(() => { + set(this.labelMap, [labelId], { ...this.labelMap[labelId], ...data }); + }); + + const response = await this.issueLabelService.patchIssueLabel(workspaceSlug, projectId, labelId, data); + return response; + } catch (error) { + console.log("Failed to update label from project store"); + runInAction(() => { + set(this.labelMap, [labelId], originalLabel); + }); + throw error; + } + }; + + /** + * updates the sort order of a label and updates the label information using API. + * @param workspaceSlug + * @param projectId + * @param labelId + * @param parentId + * @param index + * @param isSameParent + * @param prevIndex + * @returns + */ + updateLabelPosition = async ( + workspaceSlug: string, + projectId: string, + labelId: string, + parentId: string | null | undefined, + index: number, + isSameParent: boolean, + prevIndex: number | undefined + ) => { + const currLabel = this.labelMap?.[labelId]; + const labelTree = this.projectLabelsTree; + + let currentArray: IIssueLabel[]; + + if (!currLabel || !labelTree) return; + + const data: Partial = { parent: parentId }; + //find array in which the label is to be added + if (!parentId) currentArray = labelTree; + else currentArray = labelTree?.find((label) => label.id === parentId)?.children || []; + + //Add the array at the destination + if (isSameParent && prevIndex !== undefined) currentArray.splice(prevIndex, 1); + + currentArray.splice(index, 0, currLabel); + + //if currently adding to a new array, then let backend assign a sort order + if (currentArray.length > 1) { + let prevSortOrder: number | undefined, nextSortOrder: number | undefined; + + if (typeof currentArray[index - 1] !== "undefined") { + prevSortOrder = currentArray[index - 1].sort_order; + } + + if (typeof currentArray[index + 1] !== "undefined") { + nextSortOrder = currentArray[index + 1].sort_order; + } + + let sortOrder: number; + + //based on the next and previous labelMap calculate current sort order + if (prevSortOrder && nextSortOrder) { + sortOrder = (prevSortOrder + nextSortOrder) / 2; + } else if (nextSortOrder) { + sortOrder = nextSortOrder + 10000; + } else { + sortOrder = prevSortOrder! / 2; + } + + data.sort_order = sortOrder; + } + + return this.updateLabel(workspaceSlug, projectId, labelId, data); + }; + + /** + * Delete the label from the project and remove it from the labelMap object + * @param workspaceSlug + * @param projectId + * @param labelId + */ + deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => { + const originalLabel = this.labelMap[labelId]; + + try { + if (!this.labelMap[labelId]) return; + + runInAction(() => { + delete this.labelMap[labelId]; + }); + + // deleting using api + await this.issueLabelService.deleteIssueLabel(workspaceSlug, projectId, labelId); + } catch (error) { + console.log("Failed to delete label from project store"); + // reverting back to original label list + runInAction(() => { + set(this.labelMap, [labelId], originalLabel); + }); + } + }; +} diff --git a/web/store/label/workspace-label.store.ts b/web/store/label/workspace-label.store.ts new file mode 100644 index 000000000..3a1b9d5b7 --- /dev/null +++ b/web/store/label/workspace-label.store.ts @@ -0,0 +1,63 @@ +import { action, computed, makeObservable, runInAction } from "mobx"; +import { set } from "lodash"; +// services +import { IssueLabelService } from "services/issue"; +// types +import { RootStore } from "store/root.store"; +import { IIssueLabel } from "types"; + +export interface IWorkspaceLabelStore { + // computed + workspaceLabels: IIssueLabel[] | undefined; + // actions + fetchWorkspaceLabels: (workspaceSlug: string) => Promise; +} + +export class WorkspaceLabelStore implements IWorkspaceLabelStore { + // root store + rootStore; + // root store labelMap + labelMap: Record = {}; + // services + issueLabelService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // computed + workspaceLabels: computed, + // actions + fetchWorkspaceLabels: action, + }); + + // root store + this.rootStore = _rootStore; + this.labelMap = this.rootStore.labelRoot.labelMap; + // services + this.issueLabelService = new IssueLabelService(); + } + + /** + * Returns the labelMap belongs to a specific workspace + */ + get workspaceLabels() { + const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; + if (!currentWorkspaceDetails) return; + return Object.values(this.labelMap).filter((label) => label.workspace === currentWorkspaceDetails.id); + } + + /** + * Fetches all the labelMap belongs to a specific project + * @param workspaceSlug + * @param projectId + * @returns Promise + */ + fetchWorkspaceLabels = async (workspaceSlug: string) => { + const response = await this.issueLabelService.getWorkspaceIssueLabels(workspaceSlug); + runInAction(() => { + response.forEach((label) => { + set(this.labelMap, [label.id], label); + }); + }); + return response; + }; +} diff --git a/web/store/module.store.ts b/web/store/module.store.ts index a0f999d27..8b636aed0 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -1,5 +1,5 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import set from "lodash/set"; +import { set } from "lodash"; // services import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 3fd2123be..4819f70d2 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -8,22 +8,20 @@ import { IssueLabelService, IssueService } from "services/issue"; import { ProjectService, ProjectStateService } from "services/project"; export interface IProjectStore { + // states loader: boolean; error: any | null; - + // observables searchQuery: string; - projectId: string | null; projectMap: { [projectId: string]: IProject; // projectId: project Info }; - // computed searchedProjects: string[]; workspaceProjects: string[] | null; joinedProjects: string[]; favoriteProjects: string[]; currentProjectDetails: IProject | undefined; - // actions setSearchQuery: (query: string) => void; getProjectById: (projectId: string) => IProject | null; @@ -43,15 +41,14 @@ export interface IProjectStore { } export class ProjectStore implements IProjectStore { + // states loader: boolean = false; error: any | null = null; - - projectId: string | null = null; + // observables searchQuery: string = ""; projectMap: { [projectId: string]: IProject; // projectId: project Info } = {}; - // root store rootStore: RootStore; // service @@ -62,24 +59,19 @@ export class ProjectStore implements IProjectStore { constructor(_rootStore: RootStore) { makeObservable(this, { - // observable + // states loader: observable.ref, error: observable.ref, - + // observables searchQuery: observable.ref, - projectId: observable.ref, projectMap: observable, - // computed searchedProjects: computed, workspaceProjects: computed, - currentProjectDetails: computed, - joinedProjects: computed, favoriteProjects: computed, - - // action + // actions setSearchQuery: action, fetchProjects: action, fetchProjectDetails: action, diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 0687b5194..36873c099 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -6,11 +6,11 @@ import { CycleStore, ICycleStore } from "./cycle.store"; import { IProjectViewsStore, ProjectViewsStore } from "./project-view.store"; import { IModuleStore, ModulesStore } from "./module.store"; import { IUserStore, UserStore } from "./user"; -import { ILabelStore, LabelStore } from "./label.store"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; import { IStateStore, StateStore } from "./state.store"; import { IPageStore, PageStore } from "./page.store"; +import { ILabelRootStore, LabelRootStore } from "./label"; enableStaticRendering(typeof window === "undefined"); @@ -19,11 +19,11 @@ export class RootStore { user: IUserStore; workspaceRoot: IWorkspaceRootStore; projectRoot: IProjectRootStore; + labelRoot: ILabelRootStore; cycle: ICycleStore; module: IModuleStore; projectView: IProjectViewsStore; page: IPageStore; - label: ILabelStore; issue: IIssueRootStore; state: IStateStore; @@ -32,8 +32,8 @@ export class RootStore { this.user = new UserStore(this); this.workspaceRoot = new WorkspaceRootStore(this); this.projectRoot = new ProjectRootStore(this); + this.labelRoot = new LabelRootStore(this); // independent stores - this.label = new LabelStore(this); this.state = new StateStore(this); this.issue = new IssueRootStore(this); this.cycle = new CycleStore(this); diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 9daf8dbb0..d3a12024f 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -42,8 +42,8 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // root store rootStore; // sub-stores - webhook: WebhookStore; - apiToken: ApiTokenStore; + webhook: IWebhookStore; + apiToken: IApiTokenStore; constructor(_rootStore: RootStore) { makeObservable(this, { diff --git a/web/store_legacy/cycle/cycles.store.ts b/web/store_legacy/cycle/cycles.store.ts index 8e233c8f8..96122ec14 100644 --- a/web/store_legacy/cycle/cycles.store.ts +++ b/web/store_legacy/cycle/cycles.store.ts @@ -295,6 +295,19 @@ export class CycleStore implements ICycleStore { const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId); const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + + runInAction(() => { + ["all", "current", "completed", "upcoming", "draft"].forEach((view) => { + this.cycles = { + ...this.cycles, + [projectId]: { + ...this.cycles[projectId], + [view]: this.cycles[projectId][view]?.filter((c) => c.id !== cycleId), + }, + }; + }); + }); + this.fetchCycles(workspaceSlug, projectId, _currentView); return _response; diff --git a/web/store_legacy/project/project-state.store.ts b/web/store_legacy/project/project-state.store.ts index 2ac6be8ff..1cb1aeb42 100644 --- a/web/store_legacy/project/project-state.store.ts +++ b/web/store_legacy/project/project-state.store.ts @@ -4,6 +4,8 @@ import { RootStore } from "../root"; import { IState } from "types"; // services import { ProjectService, ProjectStateService } from "services/project"; +// helpers +import { sortStates } from "helpers/state.helper"; import { groupByField } from "helpers/array.helper"; export interface IProjectStateStore { @@ -77,7 +79,7 @@ export class ProjectStateStore implements IProjectStateStore { if (!this.rootStore.project.projectId) return null; const states = this.states[this.rootStore.project.projectId]; if (!states) return null; - return states; + return sortStates(states); } projectStateIds = () => { diff --git a/web/styles/globals.css b/web/styles/globals.css index e94a751f3..5d083b490 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -540,3 +540,5 @@ div.web-view-spinner div.bar12 { animation-delay: -0.0833s; -webkit-animation-delay: -0.0833s; } +[data-rfd-draggable-id] { will-change: transform, opacity; } +