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 a690124db..7733e4b5c 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): @@ -677,6 +677,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/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 1b8878995..33254614c 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -78,7 +78,7 @@ export const ActiveCycleDetails: React.FC = observer((props // toast alert const { setToastAlert } = useToast(); - useSWR( + const { isLoading } = useSWR( workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null ); @@ -97,7 +97,7 @@ export const ActiveCycleDetails: React.FC = observer((props // : null // ) as { data: IIssue[] | undefined }; - if (!cycle) + if (!cycle && isLoading) return ( diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 4ae5c9d8b..fa7fef008 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -30,6 +30,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"; @@ -53,6 +55,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycle: cycleDetailsStore, trackEvent: { setTrackElement }, + user: { currentProjectRole }, } = useMobxStore(); const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; @@ -270,10 +273,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 +290,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 +318,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { - {!isCompleted && ( + {!isCompleted && isEditingAllowed && ( { @@ -349,8 +355,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} @@ -373,10 +381,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 +393,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { <> {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} @@ -409,10 +419,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..9c821116c 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -156,7 +156,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)} +
))} @@ -193,20 +196,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..07913c65a 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -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"; @@ -144,7 +144,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { - + {moduleDetails?.name && truncateText(moduleDetails.name, 40)} } @@ -157,7 +157,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)} +
))}
@@ -194,20 +197,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..a904453e5 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -139,7 +139,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,7 +156,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + + { 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..3a0faf248 100644 --- a/web/components/inbox/main-content.tsx +++ b/web/components/inbox/main-content.tsx @@ -165,16 +165,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 +225,7 @@ export const InboxMainContent: React.FC = observer(() => { ) : null} -
+
{currentIssueState && ( = (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..ed7f73358 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -10,6 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider"; 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; @@ -26,6 +28,7 @@ export const ModuleEmptyState: React.FC = observer((props) => { moduleIssues: moduleIssueStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement }, + user: { currentProjectRole: userRole }, } = useMobxStore(); const { setToastAlert } = useToast(); @@ -44,6 +47,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { ); }; + const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + return ( <> = 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..7ff8056b9 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,5 @@ import { observer } from "mobx-react-lite"; - +import { useMobxStore } from "lib/mobx/store-provider"; // components import { AppliedDateFilters, @@ -16,6 +16,8 @@ import { X } from "lucide-react"; 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; @@ -33,10 +35,16 @@ const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props; + const { + user: { currentProjectRole }, + } = useMobxStore(); + 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/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..4cb4d74a1 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -572,7 +572,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/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/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 && ( + ) : ( + + ))} - {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..d293bbe73 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -13,6 +13,8 @@ import { Input, Loader } from "@plane/ui"; 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(() => { const [query, setQuery] = useState(""); @@ -20,10 +22,16 @@ export const ProjectViewsList = observer(() => { const router = useRouter(); const { projectId } = router.query; - const { projectViews: projectViewsStore, commandPalette: commandPaletteStore } = useMobxStore(); + const { + projectViews: projectViewsStore, + commandPalette: commandPaletteStore, + user: { currentProjectRole }, + } = useMobxStore(); const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + if (!viewsList) return ( @@ -73,6 +81,7 @@ export const ProjectViewsList = observer(() => { text: "Build your first view", onClick: () => commandPaletteStore.toggleCreateViewModal(true), }} + disabled={!isEditingAllowed} /> )} 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/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/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/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/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 982dbf27d..c987408b0 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -19,6 +19,7 @@ import { TCycleView, TCycleLayout } from "types"; import { NextPageWithLayout } from "types/app"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EUserWorkspaceRoles } from "constants/workspace"; // lib cookie import { setLocalStorage, getLocalStorage } from "lib/local-storage"; import { NewEmptyState } from "components/common/new-empty-state"; @@ -27,7 +28,10 @@ import { NewEmptyState } from "components/common/new-empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); // store - const { cycle: cycleStore } = useMobxStore(); + const { + cycle: cycleStore, + user: { currentProjectRole }, + } = useMobxStore(); const { projectCycles } = cycleStore; // router const router = useRouter(); @@ -75,6 +79,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const cycleLayout = cycleStore?.cycleLayout; const totalCycles = projectCycles?.length ?? 0; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + if (!workspaceSlug || !projectId) return null; return ( @@ -104,6 +110,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { setCreateModal(true); }, }} + disabled={!isEditingAllowed} />
) : ( diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 02317799f..d943f4b9a 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,6 +1,5 @@ import { ReactElement } from "react"; import useSWR from "swr"; -import { useRouter } from "next/router"; import Link from "next/link"; // services import { UserService } from "services/user.service"; @@ -23,9 +22,6 @@ import { NextPageWithLayout } from "types/app"; const userService = new UserService(); const ProfileActivityPage: NextPageWithLayout = () => { - 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/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 = () => {