diff --git a/apps/app/.env.example b/.env.example similarity index 66% rename from apps/app/.env.example rename to .env.example index 9e41ba88d..118a94883 100644 --- a/apps/app/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ # Replace with your instance Public IP -# NEXT_PUBLIC_API_BASE_URL = "http://localhost" NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= NEXT_PUBLIC_GOOGLE_CLIENTID="" NEXT_PUBLIC_GITHUB_APP_NAME="" @@ -10,3 +9,12 @@ NEXT_PUBLIC_ENABLE_SENTRY=0 NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 NEXT_PUBLIC_TRACK_EVENTS=0 NEXT_PUBLIC_SLACK_CLIENT_ID="" +EMAIL_HOST="" +EMAIL_HOST_USER="" +EMAIL_HOST_PASSWORD="" +AWS_REGION="" +AWS_ACCESS_KEY_ID="" +AWS_SECRET_ACCESS_KEY="" +AWS_S3_BUCKET_NAME="" +OPENAI_API_KEY="" +GPT_ENGINE="" \ No newline at end of file diff --git a/.github/workflows/push-image-backend.yml b/.github/workflows/push-image-backend.yml index abb833922..95d93f813 100644 --- a/.github/workflows/push-image-backend.yml +++ b/.github/workflows/push-image-backend.yml @@ -1,4 +1,4 @@ -name: Build Api Server Docker Image +name: Build and Push Backend Docker Image on: push: @@ -10,11 +10,8 @@ on: jobs: build_push_backend: - name: Build Api Server Docker Image + name: Build and Push Api Server Docker Image runs-on: ubuntu-20.04 - permissions: - contents: read - packages: write steps: - name: Check out the repo @@ -28,20 +25,33 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2.5.0 - - name: Login to Github Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v2.1.0 with: registry: "ghcr.io" username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + registry: "registry.hub.docker.com" + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) + id: ghmeta + uses: docker/metadata-action@v4.3.0 + with: + images: makeplane/plane-backend + + - name: Extract metadata (tags, labels) for Docker (Github) + id: dkrmeta uses: docker/metadata-action@v4.3.0 with: images: ghcr.io/${{ github.repository }}-backend - - name: Build Api Server + - name: Build and Push to GitHub Container Registry uses: docker/build-push-action@v4.0.0 with: context: ./apiserver @@ -50,5 +60,18 @@ jobs: push: true cache-from: type=gha cache-to: type=gha - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.ghmeta.outputs.tags }} + labels: ${{ steps.ghmeta.outputs.labels }} + + - name: Build and Push to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: ./apiserver + file: ./apiserver/Dockerfile.api + platforms: linux/arm64,linux/amd64 + push: true + cache-from: type=gha + cache-to: type=gha + tags: ${{ steps.dkrmeta.outputs.tags }} + labels: ${{ steps.dkrmeta.outputs.labels }} + diff --git a/.github/workflows/push-image-frontend.yml b/.github/workflows/push-image-frontend.yml index c6a3bf1b8..cbd742511 100644 --- a/.github/workflows/push-image-frontend.yml +++ b/.github/workflows/push-image-frontend.yml @@ -1,4 +1,4 @@ -name: Build Frontend Docker Image +name: Build and Push Frontend Docker Image on: push: @@ -12,9 +12,6 @@ jobs: build_push_frontend: name: Build Frontend Docker Image runs-on: ubuntu-20.04 - permissions: - contents: read - packages: write steps: - name: Check out the repo @@ -35,13 +32,26 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + registry: "registry.hub.docker.com" + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) + id: ghmeta + uses: docker/metadata-action@v4.3.0 + with: + images: makeplane/plane-frontend + + - name: Extract metadata (tags, labels) for Docker (Github) id: meta uses: docker/metadata-action@v4.3.0 with: images: ghcr.io/${{ github.repository }}-frontend - - name: Build Frontend Server + - name: Build and Push to GitHub Container Registry uses: docker/build-push-action@v4.0.0 with: context: . @@ -50,5 +60,18 @@ jobs: push: true cache-from: type=gha cache-to: type=gha - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.ghmeta.outputs.tags }} + labels: ${{ steps.ghmeta.outputs.labels }} + + - name: Build and Push to Docker Container Registry + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./apps/app/Dockerfile.web + platforms: linux/arm64,linux/amd64 + push: true + cache-from: type=gha + cache-to: type=gha + tags: ${{ steps.dkrmeta.outputs.tags }} + labels: ${{ steps.dkrmeta.outputs.labels }} + diff --git a/Dockerfile b/Dockerfile index 094d628e3..cb7ef6887 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat RUN apk update # Set working directory WORKDIR /app +ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -16,7 +17,7 @@ FROM node:18-alpine AS installer RUN apk add --no-cache libc6-compat RUN apk update WORKDIR /app - +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 # First install the dependencies (as they change less often) COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . @@ -26,9 +27,16 @@ RUN yarn install # Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json +COPY replace-env-vars.sh /usr/local/bin/ +USER root +RUN chmod +x /usr/local/bin/replace-env-vars.sh RUN yarn turbo run build --filter=app +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} FROM python:3.11.1-alpine3.17 AS backend @@ -108,6 +116,16 @@ COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf COPY nginx/supervisor.conf /code/supervisor.conf +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +USER root +COPY replace-env-vars.sh /usr/local/bin/ +COPY start.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/replace-env-vars.sh +RUN chmod +x /usr/local/bin/start.sh + CMD ["supervisord","-c","/code/supervisor.conf"] diff --git a/README.md b/README.md index 102739e4e..a7a23d7c5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@

-Meet Plane. An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind πŸ§˜β€β™€οΈ. +Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind πŸ§˜β€β™€οΈ. > 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. @@ -58,11 +58,18 @@ cd plane > If running in a cloud env replace localhost with public facing IP address of the VM +- Export Environment Variables + +```bash +set -a +source .env +set +a +``` - Run Docker compose up ```bash -docker-compose up +docker-compose -f docker-compose-hub.yml up ``` You can use the default email and password for your first login `captain@plane.so` and `password123`. diff --git a/apiserver/.env.example b/apiserver/.env.example deleted file mode 100644 index 8a7c76ffa..000000000 --- a/apiserver/.env.example +++ /dev/null @@ -1,28 +0,0 @@ -DJANGO_SETTINGS_MODULE="plane.settings.production" -# Database -DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane -# Cache -REDIS_URL=redis://redis:6379/ -# SMTP -EMAIL_HOST="" -EMAIL_HOST_USER="" -EMAIL_HOST_PASSWORD="" -EMAIL_PORT="587" -EMAIL_USE_TLS="1" -EMAIL_FROM="Team Plane " -# AWS -AWS_REGION="" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -AWS_S3_BUCKET_NAME="" -AWS_S3_ENDPOINT_URL="" -# FE -WEB_URL="localhost/" -# OAUTH -GITHUB_CLIENT_SECRET="" -# Flags -DISABLE_COLLECTSTATIC=1 -DOCKERIZED=1 -# GPT Envs -OPENAI_API_KEY=0 -GPT_ENGINE=0 diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index 5c06a28e7..d6d281357 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -19,10 +19,29 @@ class CycleSerializer(BaseSerializer): started_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True) + assignees = serializers.SerializerMethodField() workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") project_detail = ProjectLiteSerializer(read_only=True, source="project") + def get_assignees(self, obj): + members = [ + { + "avatar": assignee.avatar, + "first_name": assignee.first_name, + "id": assignee.id, + } + for issue_cycle in obj.issue_cycle.all() + for assignee in issue_cycle.issue.assignees.all() + ] + # Use a set comprehension to return only the unique objects + unique_objects = {frozenset(item.items()) for item in members} + + # Convert the set back to a list of dictionaries + unique_list = [dict(item) for item in unique_objects] + + return unique_list + class Meta: model = Cycle fields = "__all__" diff --git a/apiserver/plane/api/serializers/estimate.py b/apiserver/plane/api/serializers/estimate.py index 360275562..3cb0e4713 100644 --- a/apiserver/plane/api/serializers/estimate.py +++ b/apiserver/plane/api/serializers/estimate.py @@ -2,9 +2,13 @@ from .base import BaseSerializer from plane.db.models import Estimate, EstimatePoint +from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer class EstimateSerializer(BaseSerializer): + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + class Meta: model = Estimate fields = "__all__" @@ -27,6 +31,8 @@ class EstimatePointSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer): points = EstimatePointSerializer(read_only=True, many=True) + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = Estimate diff --git a/apiserver/plane/api/serializers/importer.py b/apiserver/plane/api/serializers/importer.py index fcc7da6ce..8997f6392 100644 --- a/apiserver/plane/api/serializers/importer.py +++ b/apiserver/plane/api/serializers/importer.py @@ -2,12 +2,14 @@ from .base import BaseSerializer from .user import UserLiteSerializer from .project import ProjectLiteSerializer +from .workspace import WorkspaceLiteSerializer from plane.db.models import Importer class ImporterSerializer(BaseSerializer): initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True) + workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) class Meta: model = Importer diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 9265aca00..f61a93487 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -30,6 +30,7 @@ from plane.db.models import ( CycleFavorite, IssueLink, IssueAttachment, + User, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -413,10 +414,11 @@ class CycleDateCheckEndpoint(BaseAPIView): try: start_date = request.data.get("start_date", False) end_date = request.data.get("end_date", False) + cycle_id = request.data.get("cycle_id", False) if not start_date or not end_date: return Response( - {"error": "Start date and end date both are required"}, + {"error": "Start date and end date are required"}, status=status.HTTP_400_BAD_REQUEST, ) @@ -428,6 +430,11 @@ class CycleDateCheckEndpoint(BaseAPIView): project_id=project_id, ) + if cycle_id: + cycles = cycles.filter( + ~Q(pk=cycle_id), + ) + if cycles.exists(): return Response( { @@ -501,6 +508,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView): filter=Q(issue_cycle__issue__state__group="backlog"), ) ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only("avatar", "first_name", "id").distinct(), + ) + ) .order_by("name", "-is_favorite") ) @@ -545,6 +558,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView): filter=Q(issue_cycle__issue__state__group="backlog"), ) ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only("avatar", "first_name", "id").distinct(), + ) + ) .order_by("name", "-is_favorite") ) @@ -557,7 +576,7 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView): ) except Exception as e: - capture_exception(e) + print(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, @@ -618,6 +637,12 @@ class CompletedCyclesEndpoint(BaseAPIView): filter=Q(issue_cycle__issue__state__group="backlog"), ) ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only("avatar", "first_name", "id").distinct(), + ) + ) .order_by("name", "-is_favorite") ) @@ -693,6 +718,12 @@ class DraftCyclesEndpoint(BaseAPIView): filter=Q(issue_cycle__issue__state__group="backlog"), ) ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only("avatar", "first_name", "id").distinct(), + ) + ) .order_by("name", "-is_favorite") ) diff --git a/apiserver/plane/api/views/estimate.py b/apiserver/plane/api/views/estimate.py index e878ccafc..68de54d7a 100644 --- a/apiserver/plane/api/views/estimate.py +++ b/apiserver/plane/api/views/estimate.py @@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet): try: estimates = Estimate.objects.filter( workspace__slug=slug, project_id=project_id - ).prefetch_related("points") + ).prefetch_related("points").select_related("workspace", "project") serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: - print(e) + capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, @@ -211,7 +211,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): try: EstimatePoint.objects.bulk_update( - updated_estimate_points, ["value"], batch_size=10 + updated_estimate_points, ["value"], batch_size=10, ) except IntegrityError as e: return Response( diff --git a/apiserver/plane/db/models/estimate.py b/apiserver/plane/db/models/estimate.py index f163a1407..d95a86316 100644 --- a/apiserver/plane/db/models/estimate.py +++ b/apiserver/plane/db/models/estimate.py @@ -39,7 +39,6 @@ class EstimatePoint(ProjectBaseModel): return f"{self.estimate.name} <{self.key}> <{self.value}>" class Meta: - unique_together = ["value", "estimate"] verbose_name = "Estimate Point" verbose_name_plural = "Estimate Points" db_table = "estimate_points" diff --git a/apps/app/Dockerfile.web b/apps/app/Dockerfile.web index 11bf98bd4..0b3e45f7a 100644 --- a/apps/app/Dockerfile.web +++ b/apps/app/Dockerfile.web @@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat RUN apk update # Set working directory WORKDIR /app +ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -12,10 +13,10 @@ RUN turbo prune --scope=app --docker # Add lockfile and package.json's of isolated subworkspace FROM node:18-alpine AS installer - RUN apk add --no-cache libc6-compat RUN apk update WORKDIR /app +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 # First install the dependencies (as they change less often) COPY .gitignore .gitignore @@ -26,9 +27,17 @@ RUN yarn install # Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json +COPY replace-env-vars.sh /usr/local/bin/ +USER root +RUN chmod +x /usr/local/bin/replace-env-vars.sh RUN yarn turbo run build --filter=app +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} + FROM node:18-alpine AS runner WORKDIR /app @@ -43,8 +52,20 @@ COPY --from=installer /app/apps/app/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./ -# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules -COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static + +COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next + +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ + BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +USER root +COPY replace-env-vars.sh /usr/local/bin/ +COPY start.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/replace-env-vars.sh +RUN chmod +x /usr/local/bin/start.sh + +USER captain ENV NEXT_TELEMETRY_DISABLED 1 diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 9d23d84e2..bed84f5ad 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -821,7 +821,7 @@ export const CommandPalette: React.FC = () => { >
- Billings and Plans + Billing and Plans
{ >
- Import/Export + Import/ Export
diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 69346cd18..495cee0a5 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -44,7 +44,7 @@ export const AllBoards: React.FC = ({ return ( <> {groupedByIssues ? ( -
+
{Object.keys(groupedByIssues).map((singleGroup, index) => { const currentState = selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 8007f3216..e82b9897c 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -392,7 +392,7 @@ export const SingleBoardIssue: React.FC = ({
- + {issue.link_count}
@@ -402,7 +402,7 @@ export const SingleBoardIssue: React.FC = ({
- + {issue.attachment_count}
diff --git a/apps/app/components/core/calendar-view/calendar.tsx b/apps/app/components/core/calendar-view/calendar.tsx index dd814ed09..9dfdac64e 100644 --- a/apps/app/components/core/calendar-view/calendar.tsx +++ b/apps/app/components/core/calendar-view/calendar.tsx @@ -229,7 +229,7 @@ export const CalendarView: React.FC = ({ addIssueToDate }) => { return calendarIssues ? ( -
+
diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/gpt-assistant-modal.tsx index 37104e30f..653ea3e01 100644 --- a/apps/app/components/core/gpt-assistant-modal.tsx +++ b/apps/app/components/core/gpt-assistant-modal.tsx @@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC = ({ return (
@@ -138,7 +138,7 @@ export const GptAssistantModal: React.FC = ({
)} {response !== "" && ( -
+
Response: ${response}

`} diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index 5d6c909fe..6868cf7b0 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -134,7 +134,7 @@ export const IssuesFilterView: React.FC = () => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - +
{issueView !== "calendar" && ( diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 497d7fba4..9baa0318d 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -353,7 +353,7 @@ export const IssuesView: React.FC = ({ console.log(e); }); }, - [workspaceSlug, projectId, cycleId, params] + [workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert] ); const removeIssueFromModule = useCallback( @@ -396,7 +396,7 @@ export const IssuesView: React.FC = ({ console.log(e); }); }, - [workspaceSlug, projectId, moduleId, params] + [workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert] ); const handleTrashBox = useCallback( @@ -442,39 +442,35 @@ export const IssuesView: React.FC = ({ handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> - <> -
- - {areFiltersApplied && ( - { - if (viewId) { - setFilters({}, true); - setToastAlert({ - title: "View updated", - message: "Your view has been updated", - type: "success", - }); - } else - setCreateViewModal({ - query: filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!viewId && } - {viewId ? "Update" : "Save"} view - - )} -
- {areFiltersApplied && ( -
- )} - + {areFiltersApplied && ( + <> +
+ + {areFiltersApplied && ( + { + if (viewId) { + setFilters({}, true); + setToastAlert({ + title: "View updated", + message: "Your view has been updated", + type: "success", + }); + } else + setCreateViewModal({ + query: filters, + }); + }} + className="flex items-center gap-2 text-sm" + > + {!viewId && } + {viewId ? "Update" : "Save"} view + + )} +
+ {
} + + )} diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index 3f794c20c..dbcb87451 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -314,7 +314,7 @@ export const SingleListIssue: React.FC = ({
- + {issue.link_count}
@@ -324,7 +324,7 @@ export const SingleListIssue: React.FC = ({
- + {issue.attachment_count}
diff --git a/apps/app/components/cycles/completed-cycles-list.tsx b/apps/app/components/cycles/completed-cycles-list.tsx index bf1971368..6729ceeeb 100644 --- a/apps/app/components/cycles/completed-cycles-list.tsx +++ b/apps/app/components/cycles/completed-cycles-list.tsx @@ -65,7 +65,11 @@ export const CompletedCyclesList: React.FC = ({ completedCycles.completed_cycles.length > 0 ? (
- + Completed cycles are not editable.
diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index f977bc74b..a0bd781ce 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -1,21 +1,10 @@ -import { useEffect, useState } from "react"; - -import { useRouter } from "next/router"; +import { useEffect } from "react"; // react-hook-form import { Controller, useForm } from "react-hook-form"; -// services -import cyclesService from "services/cycles.service"; -// hooks -import useToast from "hooks/use-toast"; + // ui import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// helpers -import { - getDateRangeStatus, - isDateGreaterThanToday, - isDateRangeValid, -} from "helpers/date-time.helper"; // types import { ICycle } from "types"; @@ -34,13 +23,6 @@ const defaultValues: Partial = { }; export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const [isDateValid, setIsDateValid] = useState(true); - const { register, formState: { errors, isSubmitting }, @@ -60,43 +42,6 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat }); }; - const cycleStatus = - data?.start_date && data?.end_date ? getDateRangeStatus(data?.start_date, data?.end_date) : ""; - - const dateChecker = async (payload: any) => { - if (isDateGreaterThanToday(payload.end_date)) { - await cyclesService - .cycleDateCheck(workspaceSlug as string, projectId as string, payload) - .then((res) => { - if (res.status) { - setIsDateValid(true); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - }); - } - }) - .catch((err) => { - console.log(err); - }); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: "Unable to create cycle in past date. Please enter a valid date.", - }); - } - }; - - const checkEmptyDate = - (watch("start_date") === "" && watch("end_date") === "") || - (!watch("start_date") && !watch("end_date")); - useEffect(() => { reset({ ...defaultValues, @@ -147,30 +92,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat control={control} name="start_date" render={({ field: { value, onChange } }) => ( - { - onChange(val); - if (val && watch("end_date")) { - if (isDateRangeValid(val, `${watch("end_date")}`)) { - cycleStatus != "current" && - dateChecker({ - start_date: val, - end_date: watch("end_date"), - }); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } - }} - /> + onChange(val)} /> )} />
@@ -179,30 +101,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat control={control} name="end_date" render={({ field: { value, onChange } }) => ( - { - onChange(val); - if (watch("start_date") && val) { - if (isDateRangeValid(`${watch("start_date")}`, val)) { - cycleStatus != "current" && - dateChecker({ - start_date: watch("start_date"), - end_date: val, - }); - } else { - setIsDateValid(false); - setToastAlert({ - type: "error", - title: "Error!", - message: - "The date you have entered is invalid. Please check and enter a valid date.", - }); - } - } - }} - /> + onChange(val)} /> )} />
@@ -211,18 +110,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat
Cancel - + {status ? isSubmitting ? "Updating Cycle..." diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index cd439c372..968c6f46e 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { CycleForm } from "components/cycles"; // helper -import { getDateRangeStatus } from "helpers/date-time.helper"; +import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.helper"; // types import type { ICycle } from "types"; // fetch keys @@ -128,6 +128,21 @@ export const CreateUpdateCycleModal: React.FC = ({ }); }; + const dateChecker = async (payload: any) => { + try { + const res = await cycleService.cycleDateCheck( + workspaceSlug as string, + projectId as string, + payload + ); + console.log(res); + return res.status; + } catch (err) { + console.log(err); + return false; + } + }; + const handleFormSubmit = async (formData: Partial) => { if (!workspaceSlug || !projectId) return; @@ -135,8 +150,63 @@ export const CreateUpdateCycleModal: React.FC = ({ ...formData, }; - if (!data) await createCycle(payload); - else await updateCycle(data.id, payload); + if (payload.start_date && payload.end_date) { + if (!isDateGreaterThanToday(payload.end_date)) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Unable to create cycle in past date. Please enter a valid date.", + }); + return; + } + + const isDateValid = await dateChecker({ + start_date: payload.start_date, + end_date: payload.end_date, + }); + + if (data?.start_date && data?.end_date) { + const isDateValidForExistingCycle = await dateChecker({ + start_date: payload.start_date, + end_date: payload.end_date, + cycle_id: data.id, + }); + + if (isDateValidForExistingCycle) { + await updateCycle(data.id, payload); + return; + } else { + setToastAlert({ + type: "error", + title: "Error!", + message: + "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", + }); + return; + } + } + + if (isDateValid) { + if (data) { + await updateCycle(data.id, payload); + } else { + await createCycle(payload); + } + } else { + setToastAlert({ + type: "error", + title: "Error!", + message: + "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", + }); + } + } else { + if (data) { + await updateCycle(data.id, payload); + } else { + await createCycle(payload); + } + } }; return ( diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx index 863da7934..91e592e7d 100644 --- a/apps/app/components/cycles/sidebar.tsx +++ b/apps/app/components/cycles/sidebar.tsx @@ -370,7 +370,11 @@ export const CycleDetailsSidebar: React.FC = ({ ) : (
- + {cycleStatus === "upcoming" ? "Cycle is yet to start." @@ -444,7 +448,11 @@ export const CycleDetailsSidebar: React.FC = ({ ) : (
- + No issues found. Please add issue. diff --git a/apps/app/components/cycles/transfer-issues-modal.tsx b/apps/app/components/cycles/transfer-issues-modal.tsx index 366ef67f7..c857e154e 100644 --- a/apps/app/components/cycles/transfer-issues-modal.tsx +++ b/apps/app/components/cycles/transfer-issues-modal.tsx @@ -148,7 +148,11 @@ export const TransferIssuesModal: React.FC = ({ isOpen, handleClose }) => )) ) : (
- + You don’t have any current cycle. Please create one to transfer the issues. diff --git a/apps/app/components/cycles/transfer-issues.tsx b/apps/app/components/cycles/transfer-issues.tsx index 067147bed..77b1f59f1 100644 --- a/apps/app/components/cycles/transfer-issues.tsx +++ b/apps/app/components/cycles/transfer-issues.tsx @@ -39,7 +39,7 @@ export const TransferIssues: React.FC = ({ handleClick }) => { return (
- + Completed cycles are not editable.
diff --git a/apps/app/components/icons/exclamation-icon.tsx b/apps/app/components/icons/exclamation-icon.tsx index d904263f5..243329647 100644 --- a/apps/app/components/icons/exclamation-icon.tsx +++ b/apps/app/components/icons/exclamation-icon.tsx @@ -3,14 +3,14 @@ import React from "react"; import type { Props } from "./types"; export const ExclamationIcon: React.FC = ({ width, height, className }) => ( - - - - ); + + + +); diff --git a/apps/app/components/integration/github/select-repository.tsx b/apps/app/components/integration/github/select-repository.tsx index b1781b70e..69040c2d0 100644 --- a/apps/app/components/integration/github/select-repository.tsx +++ b/apps/app/components/integration/github/select-repository.tsx @@ -81,7 +81,7 @@ export const SelectRepository: React.FC = ({ {userRepositories && options.length < totalCount && (
- +
Read More @@ -124,7 +128,7 @@ const IntegrationGuide = () => { {importerServices ? ( importerServices.length > 0 ? (
-
+
{importerServices.map((service) => ( void; }; -const importersList: { [key: string]: string } = { - github: "GitHub", -}; - export const SingleImport: React.FC = ({ service, refreshing, handleDelete }) => (

- Import from {importersList[service.service]} to{" "} - {service.project_detail.name} + Import from{" "} + + {IMPORTERS_EXPORTERS_LIST.find((i) => i.provider === service.service)?.title} + {" "} + to {service.project_detail.name} { action = `${activityItem.verb} the`; } else if (activityItem.field === "estimate") { action = "updated the"; + } else if (activityItem.field === "cycles") { + action = + activityItem.new_value && activityItem.new_value !== "" + ? "set the cycle to" + : "removed the cycle"; + } else if (activityItem.field === "modules") { + action = + activityItem.new_value && activityItem.new_value !== "" + ? "set the module to" + : "removed the module"; } // for values that are after the action clause let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value; @@ -282,6 +292,18 @@ export const IssueActivitySection: React.FC = () => { value = "description"; } else if (activityItem.field === "attachment") { value = "attachment"; + } else if (activityItem.field === "cycles") { + const cycles = + activityItem.new_value && activityItem.new_value !== "" + ? activityItem.new_value + : activityItem.old_value; + value = cycles ? addSpaceIfCamelCase(cycles) : "None"; + } else if (activityItem.field === "modules") { + const modules = + activityItem.new_value && activityItem.new_value !== "" + ? activityItem.new_value + : activityItem.old_value; + value = modules ? addSpaceIfCamelCase(modules) : "None"; } else if (activityItem.field === "link") { value = "link"; } else if (activityItem.field === "estimate_point") { diff --git a/apps/app/components/issues/attachments.tsx b/apps/app/components/issues/attachments.tsx index 13fc0d972..7f4be47dd 100644 --- a/apps/app/components/issues/attachments.tsx +++ b/apps/app/components/issues/attachments.tsx @@ -82,7 +82,7 @@ export const IssueAttachments = () => { } uploaded on ${renderLongDateFormat(file.updated_at)}`} > - +

diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx index ebf063719..369cfc6e2 100644 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -82,8 +82,8 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId const isNotAllowed = false; return ( -
-
+
+
{properties?.key && ( @@ -91,13 +91,13 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId tooltipHeading="Issue ID" tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} > - + {issue.project_detail?.identifier}-{issue.sequence_id} )} - + {truncateText(issue.name, 50)} @@ -127,7 +127,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId /> )} {properties.sub_issue_count && ( -
+
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
)} @@ -136,10 +136,10 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId {issue.label_details.map((label) => ( = ({ issue, properties, projectId )} {properties.link && ( -
+
-
- +
+ {issue.link_count}
)} {properties.attachment_count && ( -
+
-
- +
+ {issue.attachment_count}
diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index c412c9fda..7db9cd906 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -78,7 +78,7 @@ export const SidebarCycleSelect: React.FC = ({ } - value={issueCycle?.cycle_detail.id} + value={issueCycle ? issueCycle.cycle_detail.id : null} onChange={(value: any) => { !value ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") diff --git a/apps/app/components/issues/sidebar-select/module.tsx b/apps/app/components/issues/sidebar-select/module.tsx index 282ccb533..d1b0f999b 100644 --- a/apps/app/components/issues/sidebar-select/module.tsx +++ b/apps/app/components/issues/sidebar-select/module.tsx @@ -82,7 +82,7 @@ export const SidebarModuleSelect: React.FC = ({ } - value={issueModule?.module_detail?.id} + value={issueModule ? issueModule.module_detail?.id : null} onChange={(value: any) => { !value ? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 3b3076e05..e5a932608 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -416,7 +416,11 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, ) : (
- + Invalid date. Please enter valid date. @@ -488,7 +492,11 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, ) : (
- + No issues found. Please add issue. diff --git a/apps/app/components/modules/single-module-card.tsx b/apps/app/components/modules/single-module-card.tsx index f7192de6f..c4a580db5 100644 --- a/apps/app/components/modules/single-module-card.tsx +++ b/apps/app/components/modules/single-module-card.tsx @@ -44,7 +44,8 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) const { setToastAlert } = useToast(); - const completionPercentage = (module.completed_issues / module.total_issues) * 100; + const completionPercentage = + ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100; const handleDeleteModule = () => { if (!module) return; diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx index 4b8fbcc99..832aa03df 100644 --- a/apps/app/components/pages/create-update-block-inline.tsx +++ b/apps/app/components/pages/create-update-block-inline.tsx @@ -15,8 +15,10 @@ import issuesService from "services/issues.service"; import aiService from "services/ai.service"; // hooks import useToast from "hooks/use-toast"; +// components +import { GptAssistantModal } from "components/core"; // ui -import { Input, Loader, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; +import { Input, Loader, PrimaryButton, SecondaryButton } from "components/ui"; // types import { IPageBlock } from "types"; // fetch-keys @@ -25,9 +27,9 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; type Props = { handleClose: () => void; data?: IPageBlock; + handleAiAssistance?: (response: string) => void; setIsSyncing?: React.Dispatch>; focus?: keyof IPageBlock; - setGptAssistantModal: () => void; }; const defaultValues = { @@ -48,11 +50,12 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor export const CreateUpdateBlockInline: React.FC = ({ handleClose, data, + handleAiAssistance, setIsSyncing, focus, - setGptAssistantModal, }) => { const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); + const [gptAssistantModal, setGptAssistantModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; @@ -230,87 +233,101 @@ export const CreateUpdateBlockInline: React.FC = ({ }, [createPageBlock, updatePageBlock, data, handleSubmit]); return ( -
-
-
- -
-
- ( - setValue("description", jsonValue)} - onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} - placeholder="Write something..." - customClassName="text-sm" - noBorder - borderOnFocus={false} - /> - )} - /> -
- - {data && ( + /> +
+ + - )} +
-
-
- Cancel - - {data - ? isSubmitting - ? "Updating..." - : "Update block" - : isSubmitting - ? "Adding..." - : "Add block"} - -
-
+
+ Cancel + + {data + ? isSubmitting + ? "Updating..." + : "Update block" + : isSubmitting + ? "Adding..." + : "Add block"} + +
+ + setGptAssistantModal(false)} + inset="top-8 left-0" + content={watch("description_html")} + htmlContent={watch("description_html")} + onResponse={(response) => { + if (data && handleAiAssistance) handleAiAssistance(response); + else { + setValue("description", {}); + setValue("description_html", `${watch("description_html")}

${response}

`); + } + }} + projectId={projectId?.toString() ?? ""} + /> +
); }; diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 34b7f6583..ff3811f66 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -2,12 +2,11 @@ import { useEffect, useState, useRef } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; -import dynamic from "next/dynamic"; import { mutate } from "swr"; // react-hook-form -import { Controller, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; // react-beautiful-dnd import { Draggable } from "react-beautiful-dnd"; // services @@ -21,7 +20,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { GptAssistantModal } from "components/core"; import { CreateUpdateBlockInline } from "components/pages"; // ui -import { CustomMenu, Loader } from "components/ui"; +import { CustomMenu } from "components/ui"; // icons import { LayerDiagonalIcon } from "components/icons"; import { ArrowPathIcon, LinkIcon } from "@heroicons/react/20/solid"; @@ -46,15 +45,6 @@ type Props = { index: number; }; -const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { - ssr: false, - loading: () => ( - - - - ), -}); - export const SinglePageBlock: React.FC = ({ block, projectDetails, index }) => { const [isSyncing, setIsSyncing] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false); @@ -291,7 +281,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index {...provided.dragHandleProps} > setGptAssistantModal((prev) => !prev)} + handleAiAssistance={handleAiAssistance} handleClose={() => setCreateBlockForm(false)} data={block} setIsSyncing={setIsSyncing} diff --git a/apps/app/components/pages/single-page-detailed-item.tsx b/apps/app/components/pages/single-page-detailed-item.tsx index 9c025f21d..4be70384e 100644 --- a/apps/app/components/pages/single-page-detailed-item.tsx +++ b/apps/app/components/pages/single-page-detailed-item.tsx @@ -162,7 +162,7 @@ export const SinglePageDetailedItem: React.FC = ({ } on ${renderLongDateFormat(`${page.created_at}`)}`} > - + diff --git a/apps/app/components/pages/single-page-list-item.tsx b/apps/app/components/pages/single-page-list-item.tsx index bb9589502..876d94102 100644 --- a/apps/app/components/pages/single-page-list-item.tsx +++ b/apps/app/components/pages/single-page-list-item.tsx @@ -161,7 +161,7 @@ export const SinglePageListItem: React.FC = ({ } on ${renderLongDateFormat(`${page.created_at}`)}`} > - + diff --git a/apps/app/components/project/index.ts b/apps/app/components/project/index.ts index 47d7a4b1c..e5ece3955 100644 --- a/apps/app/components/project/index.ts +++ b/apps/app/components/project/index.ts @@ -1,6 +1,7 @@ export * from "./create-project-modal"; export * from "./delete-project-modal"; export * from "./sidebar-list"; +export * from "./settings-header" export * from "./single-integration-card"; export * from "./single-project-card"; export * from "./single-sidebar-project"; diff --git a/apps/app/components/project/settings-header.tsx b/apps/app/components/project/settings-header.tsx new file mode 100644 index 000000000..c0de3f4c0 --- /dev/null +++ b/apps/app/components/project/settings-header.tsx @@ -0,0 +1,13 @@ +import SettingsNavbar from "layouts/settings-navbar"; + +export const SettingsHeader = () => ( +
+
+

Project Settings

+

+ This information will be displayed to every member of the project. +

+
+ +
+); diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx index a3f265032..4ff2b2cd3 100644 --- a/apps/app/components/project/single-sidebar-project.tsx +++ b/apps/app/components/project/single-sidebar-project.tsx @@ -172,8 +172,8 @@ export const SingleSidebarProject: React.FC = ({
diff --git a/apps/app/components/workspace/index.ts b/apps/app/components/workspace/index.ts index db509a70d..d40b9d58e 100644 --- a/apps/app/components/workspace/index.ts +++ b/apps/app/components/workspace/index.ts @@ -6,5 +6,6 @@ export * from "./help-section"; export * from "./issues-list"; export * from "./issues-pie-chart"; export * from "./issues-stats"; +export * from "./settings-header"; export * from "./sidebar-dropdown"; export * from "./sidebar-menu"; diff --git a/apps/app/components/workspace/settings-header.tsx b/apps/app/components/workspace/settings-header.tsx new file mode 100644 index 000000000..dd560edcd --- /dev/null +++ b/apps/app/components/workspace/settings-header.tsx @@ -0,0 +1,13 @@ +import SettingsNavbar from "layouts/settings-navbar"; + +export const SettingsHeader = () => ( +
+
+

Workspace Settings

+

+ This information will be displayed to every member of the workspace. +

+
+ +
+); diff --git a/apps/app/components/workspace/sidebar-menu.tsx b/apps/app/components/workspace/sidebar-menu.tsx index 58712a30b..8e5bc65ab 100644 --- a/apps/app/components/workspace/sidebar-menu.tsx +++ b/apps/app/components/workspace/sidebar-menu.tsx @@ -47,8 +47,8 @@ export const WorkspaceSidebarMenu: React.FC = () => { ? router.asPath === link.href : router.asPath.includes(link.href) ) - ? "bg-brand-base text-brand-base" - : "text-brand-secondary hover:bg-brand-surface-1 hover:text-brand-secondary focus:bg-brand-base focus:text-brand-secondary" + ? "bg-brand-surface-2 text-brand-base" + : "text-brand-secondary hover:bg-brand-surface-2 hover:text-brand-secondary focus:bg-brand-surface-2 focus:text-brand-secondary" } group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${ sidebarCollapse ? "justify-center" : "" }`} diff --git a/apps/app/layouts/app-layout/app-header.tsx b/apps/app/layouts/app-layout/app-header.tsx index 979cbd8cc..fb533da93 100644 --- a/apps/app/layouts/app-layout/app-header.tsx +++ b/apps/app/layouts/app-layout/app-header.tsx @@ -9,8 +9,8 @@ type Props = { }; const Header: React.FC = ({ breadcrumbs, left, right, setToggleSidebar }) => ( -
-
+
+
); }; diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index 1ad2d6868..61da09887 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -11,7 +11,6 @@ import useIssuesView from "hooks/use-issues-view"; import Container from "layouts/container"; import AppHeader from "layouts/app-layout/app-header"; import AppSidebar from "layouts/app-layout/app-sidebar"; -import SettingsNavbar from "layouts/settings-navbar"; // components import { NotAuthorizedView, JoinProject } from "components/auth-screens"; import { CommandPalette } from "components/command-palette"; @@ -30,7 +29,6 @@ type Meta = { type Props = { meta?: Meta; children: React.ReactNode; - noPadding?: boolean; noHeader?: boolean; bg?: "primary" | "secondary"; breadcrumbs?: JSX.Element; @@ -47,7 +45,6 @@ export const ProjectAuthorizationWrapper: React.FC = (props) => ( const ProjectAuthorizationWrapped: React.FC = ({ meta, children, - noPadding = false, noHeader = false, bg = "primary", breadcrumbs, @@ -68,8 +65,9 @@ const ProjectAuthorizationWrapped: React.FC = ({ return ( -
+
+ {loading ? (
@@ -107,7 +105,15 @@ const ProjectAuthorizationWrapped: React.FC = ({ type="project" /> ) : ( -
+
{!noHeader && ( = ({ setToggleSidebar={setToggleSidebar} /> )} -
- {settingsLayout && ( -
-
-

Project Settings

-

- This information will be displayed to every member of the project. -

-
- -
- )} - {children} +
+
+ {children} +
)} diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index 608760d6f..90dcbcf13 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -32,25 +32,21 @@ type Meta = { type Props = { meta?: Meta; children: React.ReactNode; - noPadding?: boolean; noHeader?: boolean; bg?: "primary" | "secondary"; breadcrumbs?: JSX.Element; left?: JSX.Element; right?: JSX.Element; - profilePage?: boolean; }; export const WorkspaceAuthorizationLayout: React.FC = ({ meta, children, - noPadding = false, noHeader = false, bg = "primary", breadcrumbs, left, right, - profilePage = false, }) => { const [toggleSidebar, setToggleSidebar] = useState(false); @@ -101,7 +97,7 @@ export const WorkspaceAuthorizationLayout: React.FC = ({ -
+
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( = ({ type="workspace" /> ) : ( -
+
{!noHeader && ( = ({ setToggleSidebar={setToggleSidebar} /> )} -
- {(settingsLayout || profilePage) && ( -
-
-

- {profilePage ? "Profile" : "Workspace"} Settings -

-

- {profilePage - ? "This information will be visible to only you." - : "This information will be displayed to every member of the workspace."} -

-
- -
- )} - {children} +
+
+ {children} +
)} diff --git a/apps/app/layouts/settings-navbar.tsx b/apps/app/layouts/settings-navbar.tsx index 027f87f61..fb720acf8 100644 --- a/apps/app/layouts/settings-navbar.tsx +++ b/apps/app/layouts/settings-navbar.tsx @@ -30,7 +30,7 @@ const SettingsNavbar: React.FC = ({ profilePage = false }) => { href: `/${workspaceSlug}/settings/integrations`, }, { - label: "Import/Export", + label: "Import/ Export", href: `/${workspaceSlug}/settings/import-export`, }, ]; diff --git a/apps/app/next.config.js b/apps/app/next.config.js index b3c67eedd..876694142 100644 --- a/apps/app/next.config.js +++ b/apps/app/next.config.js @@ -1,6 +1,10 @@ +require("dotenv").config({ path: ".env" }); + const { withSentryConfig } = require("@sentry/nextjs"); const path = require("path"); -const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "").split(",").filter((domain) => domain.length > 0); +const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "") + .split(",") + .filter((domain) => domain.length > 0); const nextConfig = { reactStrictMode: false, diff --git a/apps/app/package.json b/apps/app/package.json index d3c414210..187e356ff 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -23,6 +23,7 @@ "@types/react-datepicker": "^4.8.0", "axios": "^1.1.3", "cmdk": "^0.2.0", + "dotenv": "^16.0.3", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", "next": "12.3.2", diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx index 5edef60eb..fd3ca88d3 100644 --- a/apps/app/pages/[workspaceSlug]/index.tsx +++ b/apps/app/pages/[workspaceSlug]/index.tsx @@ -45,7 +45,7 @@ const WorkspacePage: NextPage = () => { isOpen={isProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen} /> -
+
{ } - noPadding right={
{myIssues && myIssues.length > 0 && ( @@ -52,7 +51,7 @@ const MyIssuesPage: NextPage = () => { <> View @@ -69,29 +68,27 @@ const MyIssuesPage: NextPage = () => { leaveTo="opacity-0 translate-y-1" > -
-
-

Properties

-
- {Object.keys(properties).map((key) => { - if (key === "estimate") return null; +
+

Properties

+
+ {Object.keys(properties).map((key) => { + if (key === "estimate") return null; - return ( - - ); - })} -
+ return ( + + ); + })}
@@ -107,7 +104,7 @@ const MyIssuesPage: NextPage = () => { document.dispatchEvent(e); }} > - + Add Issue
@@ -117,55 +114,42 @@ const MyIssuesPage: NextPage = () => { {myIssues ? ( <> {myIssues.length > 0 ? ( -
- - {({ open }) => ( -
-
- -
- - - -

My Issues

- - {myIssues.length} - -
-
-
- - - {myIssues.map((issue: IIssue) => ( - - ))} - - + + {({ open }) => ( +
+
+ +
+

My Issues

+ + {myIssues.length} + +
+
- )} - -
+ + + {myIssues.map((issue: IIssue) => ( + + ))} + + +
+ )} +
) : (
{ const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); @@ -25,20 +26,30 @@ const ProfileActivity = () => { } - profilePage > - {userActivity ? ( - userActivity.results.length > 0 ? ( - - ) : null - ) : ( - - - - - - - )} +
+
+
+

Profile Settings

+

+ This information will be visible to only you. +

+
+ +
+ {userActivity ? ( + userActivity.results.length > 0 ? ( + + ) : null + ) : ( + + + + + + + )} +
); }; diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index 6487fbbd8..5fbf3ef2a 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -24,6 +24,7 @@ import type { NextPage } from "next"; import type { IUser } from "types"; // constants import { USER_ROLES } from "constants/workspace"; +import SettingsNavbar from "layouts/settings-navbar"; const defaultValues: Partial = { avatar: "", @@ -130,7 +131,6 @@ const Profile: NextPage = () => { } - profilePage > { userImage /> {myProfile ? ( -
-
-
-

Profile Picture

-

- Max file size is 5MB. Supported file types are .jpg and .png. +

+
+
+

Profile Settings

+

+ This information will be visible to only you.

-
-
- -
- { - setIsImageUploadModalOpen(true); - }} - > - Upload - - {myProfile.avatar && myProfile.avatar !== "" && ( - handleDelete(myProfile.avatar, true)} - loading={isRemoving} + +
+
+
+
+

Profile Picture

+

+ Max file size is 5MB. Supported file types are .jpg and .png. +

+
+
+
+ +
+ { + setIsImageUploadModalOpen(true); + }} > - {isRemoving ? "Removing..." : "Remove"} - - )} + Upload + + {myProfile.avatar && myProfile.avatar !== "" && ( + handleDelete(myProfile.avatar, true)} + loading={isRemoving} + > + {isRemoving ? "Removing..." : "Remove"} + + )} +
-
-
-
-

Full Name

-

- This name will be reflected on all the projects you are working on. -

+
+
+

Full Name

+

+ This name will be reflected on all the projects you are working on. +

+
+
+ + +
-
- - +
+
+

Email

+

+ The email address that you are using. +

+
+
+ +
-
-
-
-

Email

-

The email address that you are using.

+
+
+

Role

+

Add your role.

+
+
+ ( + + {USER_ROLES.map((item) => ( + + {item.label} + + ))} + + )} + /> +
-
- +
+
+

Theme

+

+ Select or customize your interface color scheme. +

+
+
+ +
-
-
-
-

Role

-

Add your role.

+
+ + {isSubmitting ? "Updating..." : "Update profile"} +
-
- ( - - {USER_ROLES.map((item) => ( - - {item.label} - - ))} - - )} - /> -
-
-
-
-

Theme

-

- Select or customize your interface color scheme. -

-
-
- -
-
-
- - {isSubmitting ? "Updating..." : "Update profile"} -
) : ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 55351b42d..cf4b8b109 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -124,7 +124,7 @@ const ProjectCycles: NextPage = () => { handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycle} /> -
+
{currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && (

Current Cycle

diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 85ea7551c..1d73b2a75 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -122,7 +122,6 @@ const IssueDetailsPage: NextPage = () => { return ( { document.dispatchEvent(e); }} > - + Add Module } @@ -89,7 +89,7 @@ const ProjectModules: NextPage = () => { /> {modules ? ( modules.length > 0 ? ( -
+

Modules

diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index dbdd3c57f..fb82b7364 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -312,7 +312,7 @@ const SinglePage: NextPage = () => { } > {pageDetails ? ( -
+
)} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 8dcb636a1..0ef706cbb 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -25,7 +25,7 @@ import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "compone import { Input, PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import {ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; // types import { IPage, TPageViewProps } from "types"; import type { NextPage } from "next"; @@ -195,7 +195,7 @@ const ProjectPages: NextPage = () => { } > -
+
= { project_lead: null, @@ -103,7 +104,8 @@ const ControlSettings: NextPage = () => { } > - + +
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 679392f41..f4192528c 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -27,6 +27,7 @@ import { IEstimate, IProject } from "types"; import type { NextPage } from "next"; // fetch-keys import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { SettingsHeader } from "components/project"; const EstimatesSettings: NextPage = () => { const [estimateFormOpen, setEstimateFormOpen] = useState(false); @@ -98,6 +99,14 @@ const EstimatesSettings: NextPage = () => { return ( <> + { + setEstimateFormOpen(false); + setEstimateToUpdate(undefined); + }} + /> @@ -109,68 +118,63 @@ const EstimatesSettings: NextPage = () => { } > - { - setEstimateFormOpen(false); - setEstimateToUpdate(undefined); - }} - /> -
-

Estimates

-
-
- { - setEstimateToUpdate(undefined); - setEstimateFormOpen(true); - }} - > - - Create New Estimate - - {projectDetails?.estimate && ( - Disable Estimates - )} +
+ +
+

Estimates

+
+
+ { + setEstimateToUpdate(undefined); + setEstimateFormOpen(true); + }} + > + + Create New Estimate + + {projectDetails?.estimate && ( + Disable Estimates + )} +
-
-
- {estimatesList ? ( - estimatesList.length > 0 ? ( -
- {estimatesList.map((estimate) => ( - editEstimate(estimate)} - handleEstimateDelete={(estimateId) => removeEstimate(estimateId)} +
+ {estimatesList ? ( + estimatesList.length > 0 ? ( +
+ {estimatesList.map((estimate) => ( + editEstimate(estimate)} + handleEstimateDelete={(estimateId) => removeEstimate(estimateId)} + /> + ))} +
+ ) : ( +
+ { + setEstimateToUpdate(undefined); + setEstimateFormOpen(true); + }} /> - ))} - +
+ ) ) : ( -
- { - setEstimateToUpdate(undefined); - setEstimateFormOpen(true); - }} - /> -
- ) - ) : ( - - - - - - - )} + + + + + + + )} +
); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index 3b5a09e6c..9df67612b 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -22,6 +22,7 @@ import { IProject } from "types"; import type { NextPage } from "next"; // fetch-keys import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { SettingsHeader } from "components/project"; const featuresList = [ { @@ -134,54 +135,57 @@ const FeaturesSettings: NextPage = () => { } > -
-

Features

-
- {featuresList.map((feature) => ( -
-
- {feature.icon} -
-

{feature.title}

-

{feature.description}

+
+ +
+

Features

+
+ {featuresList.map((feature) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.description}

+
+ { + trackEventServices.trackMiscellaneousEvent( + { + workspaceId: (projectDetails?.workspace as any)?.id, + workspaceSlug, + projectId, + projectIdentifier: projectDetails?.identifier, + projectName: projectDetails?.name, + }, + !projectDetails?.[feature.property as keyof IProject] + ? getEventType(feature.title, true) + : getEventType(feature.title, false) + ); + handleSubmit({ + [feature.property]: !projectDetails?.[feature.property as keyof IProject], + }); + }} + size="lg" + />
- { - trackEventServices.trackMiscellaneousEvent( - { - workspaceId: (projectDetails?.workspace as any)?.id, - workspaceSlug, - projectId, - projectIdentifier: projectDetails?.identifier, - projectName: projectDetails?.name, - }, - !projectDetails?.[feature.property as keyof IProject] - ? getEventType(feature.title, true) - : getEventType(feature.title, false) - ); - handleSubmit({ - [feature.property]: !projectDetails?.[feature.property as keyof IProject], - }); - }} - size="lg" - /> -
- ))} -
- -
+ ))} +
+ + +
); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 1996904a8..684465f8d 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // services import projectService from "services/project.service"; // components -import { DeleteProjectModal } from "components/project"; +import { DeleteProjectModal, SettingsHeader } from "components/project"; import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; // hooks @@ -151,7 +151,8 @@ const GeneralSettings: NextPage = () => { router.push(`/${workspaceSlug}/projects`); }} /> - + +
@@ -222,7 +223,7 @@ const GeneralSettings: NextPage = () => {

Cover Photo

-

+

Select your cover photo from the given library.

diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 101044f5c..5f5077a0e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -10,12 +10,13 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import IntegrationService from "services/integration"; import projectService from "services/project.service"; // components -import { SingleIntegration } from "components/project"; +import { SettingsHeader, SingleIntegration } from "components/project"; // ui import { EmptySpace, EmptySpaceItem, Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline"; +import { ExclamationIcon } from "components/icons"; // types import { IProject } from "types"; import type { NextPage } from "next"; @@ -53,44 +54,61 @@ const ProjectIntegrations: NextPage = () => { } > - {workspaceIntegrations ? ( - workspaceIntegrations.length > 0 ? ( -
-

Integrations

-
- {workspaceIntegrations.map((integration) => ( - + + {workspaceIntegrations ? ( + workspaceIntegrations.length > 0 ? ( +
+
+

Integrations

+
+ +

+ Integrations and importers are only available on the cloud version. We plan to + open-source our SDKs in the near future so that the community can request or + contribute integrations as needed. +

+
+
+
+ {workspaceIntegrations.map((integration) => ( + + ))} +
+
+ ) : ( +
+ + { + router.push(`/${workspaceSlug}/settings/integrations`); + }} /> - ))} +
-
+ ) ) : ( -
- - { - router.push(`/${workspaceSlug}/settings/integrations`); - }} - /> - -
- ) - ) : ( - - - - - - - )} + + + + + + + )} +
); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index ab7f2e0bc..ef88e4912 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -7,8 +7,6 @@ import useSWR from "swr"; // services import projectService from "services/project.service"; import issuesService from "services/issues.service"; -// lib -import { requiredAdmin } from "lib/auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components @@ -24,10 +22,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; // types -import { IIssueLabels, UserAuth } from "types"; -import type { GetServerSidePropsContext, NextPage } from "next"; +import { IIssueLabels } from "types"; +import type { NextPage } from "next"; // fetch-keys import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { SettingsHeader } from "components/project"; const LabelsSettings: NextPage = () => { // create/edit label form @@ -103,39 +102,57 @@ const LabelsSettings: NextPage = () => { } > -
-
-

Labels

-

Manage the labels of this project.

- - - - New label - - -
-
- {labelForm && ( - - )} - <> - {issueLabels ? ( - issueLabels.map((label) => { - const children = issueLabels?.filter((l) => l.parent === label.id); +
+ +
+
+

Labels

+

Manage the labels of this project.

+ + + + New label + + +
+
+ {labelForm && ( + + )} + <> + {issueLabels ? ( + issueLabels.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); - if (children && children.length === 0) { - if (!label.parent) + if (children && children.length === 0) { + if (!label.parent) + return ( + addLabelToGroup(label)} + editLabel={(label) => { + editLabel(label); + scrollToRef.current?.scrollIntoView({ + behavior: "smooth", + }); + }} + handleLabelDelete={handleLabelDelete} + /> + ); + } else return ( - addLabelToGroup(label)} + labelChildren={children} + addLabelToGroup={addLabelToGroup} editLabel={(label) => { editLabel(label); scrollToRef.current?.scrollIntoView({ @@ -145,34 +162,19 @@ const LabelsSettings: NextPage = () => { handleLabelDelete={handleLabelDelete} /> ); - } else - return ( - { - editLabel(label); - scrollToRef.current?.scrollIntoView({ - behavior: "smooth", - }); - }} - handleLabelDelete={handleLabelDelete} - /> - ); - }) - ) : ( - - - - - - - )} - -
-
+ }) + ) : ( + + + + + + + )} + +
+
+
); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index d7fb1d824..88d03e538 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -27,6 +27,7 @@ import type { NextPage } from "next"; import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys"; // constants import { ROLE } from "constants/workspace"; +import { SettingsHeader } from "components/project"; const MembersSettings: NextPage = () => { const [inviteModal, setInviteModal] = useState(false); @@ -141,120 +142,123 @@ const MembersSettings: NextPage = () => { } > -
-
-

Members

- -
- {!projectMembers || !projectInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
-
- {member.avatar && member.avatar !== "" ? ( - {member.first_name} - ) : member.first_name !== "" ? ( - member.first_name.charAt(0) - ) : ( - member.email.charAt(0) - )} -
-
-

- {member.first_name} {member.last_name} -

-

{member.email}

-
-
-
- {!member.member && ( -
- Pending -
- )} - { - if (!activeWorkspace || !projectDetails) return; - - projectService - .updateProjectMember( - activeWorkspace.slug, - projectDetails.id, - member.id, - { - role: value, - } - ) - .then((res) => { - setToastAlert({ - type: "success", - message: "Member role updated successfully.", - title: "Success", - }); - mutateMembers( - (prevData: any) => - prevData.map((m: any) => - m.id === member.id ? { ...m, ...res, role: value } : m - ), - false - ); - }) - .catch((err) => { - console.log(err); - }); - }} - position="right" - > - {Object.keys(ROLE).map((key) => ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ))} - - - { - if (member.member) setSelectedRemoveMember(member.id); - else setSelectedInviteRemoveMember(member.id); - }} - > - - - Remove member - - - -
-
- )) - : null} +
+ +
+
+

Members

+
- )} -
+ {!projectMembers || !projectInvitations ? ( + + + + + + + ) : ( +
+ {members.length > 0 + ? members.map((member) => ( +
+
+
+ {member.avatar && member.avatar !== "" ? ( + {member.first_name} + ) : member.first_name !== "" ? ( + member.first_name.charAt(0) + ) : ( + member.email.charAt(0) + )} +
+
+

+ {member.first_name} {member.last_name} +

+

{member.email}

+
+
+
+ {!member.member && ( +
+ Pending +
+ )} + { + if (!activeWorkspace || !projectDetails) return; + + projectService + .updateProjectMember( + activeWorkspace.slug, + projectDetails.id, + member.id, + { + role: value, + } + ) + .then((res) => { + setToastAlert({ + type: "success", + message: "Member role updated successfully.", + title: "Success", + }); + mutateMembers( + (prevData: any) => + prevData.map((m: any) => + m.id === member.id ? { ...m, ...res, role: value } : m + ), + false + ); + }) + .catch((err) => { + console.log(err); + }); + }} + position="right" + > + {Object.keys(ROLE).map((key) => ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ))} + + + { + if (member.member) setSelectedRemoveMember(member.id); + else setSelectedInviteRemoveMember(member.id); + }} + > + + + Remove member + + + +
+
+ )) + : null} +
+ )} +
+
); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index b8040c473..39d83616d 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -28,6 +28,7 @@ import { getStatesList, orderStateGroups } from "helpers/state.helper"; import type { NextPage } from "next"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; +import { SettingsHeader } from "components/project"; const StatesSettings: NextPage = () => { const [activeGroup, setActiveGroup] = useState(null); @@ -66,79 +67,82 @@ const StatesSettings: NextPage = () => { } > -
-
-

States

-

Manage the states of this project.

-
-
- {states && projectDetails ? ( - Object.keys(orderedStateGroups).map((key) => { - if (orderedStateGroups[key].length !== 0) - return ( -
-
-

{key}

- -
-
- {key === activeGroup && ( - { - setActiveGroup(null); - setSelectedState(null); - }} - data={null} - selectedGroup={key as keyof StateGroup} - /> - )} - {orderedStateGroups[key].map((state, index) => - state.id !== selectedState ? ( - setSelectedState(state.id)} - handleDeleteState={() => setSelectDeleteState(state.id)} +
+ +
+
+

States

+

Manage the states of this project.

+
+
+ {states && projectDetails ? ( + Object.keys(orderedStateGroups).map((key) => { + if (orderedStateGroups[key].length !== 0) + return ( +
+
+

{key}

+ +
+
+ {key === activeGroup && ( + { + setActiveGroup(null); + setSelectedState(null); + }} + data={null} + selectedGroup={key as keyof StateGroup} /> - ) : ( -
- { - setActiveGroup(null); - setSelectedState(null); - }} - data={ - statesList?.find((state) => state.id === selectedState) ?? null - } - selectedGroup={key as keyof StateGroup} + )} + {orderedStateGroups[key].map((state, index) => + state.id !== selectedState ? ( + setSelectedState(state.id)} + handleDeleteState={() => setSelectDeleteState(state.id)} /> -
- ) - )} + ) : ( +
+ { + setActiveGroup(null); + setSelectedState(null); + }} + data={ + statesList?.find((state) => state.id === selectedState) ?? null + } + selectedGroup={key as keyof StateGroup} + /> +
+ ) + )} +
-
- ); - }) - ) : ( - - - - - - - )} + ); + }) + ) : ( + + + + + + + )} +
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 06599742b..79e72b85e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -97,7 +97,7 @@ const ProjectViews: NextPage = () => { /> {views ? ( views.length > 0 ? ( -
+

Views

{views.map((view) => ( diff --git a/apps/app/pages/[workspaceSlug]/projects/index.tsx b/apps/app/pages/[workspaceSlug]/projects/index.tsx index 9cb78563f..a38c037e5 100644 --- a/apps/app/pages/[workspaceSlug]/projects/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/index.tsx @@ -83,7 +83,7 @@ const ProjectsPage: NextPage = () => { data={projects?.find((item) => item.id === deleteProject) ?? null} /> {projects ? ( - <> +
{projects.length === 0 ? ( { ))}
)} - +
) : ( diff --git a/apps/app/pages/[workspaceSlug]/settings/billing.tsx b/apps/app/pages/[workspaceSlug]/settings/billing.tsx index acdd0b9e2..0e5cc69f4 100644 --- a/apps/app/pages/[workspaceSlug]/settings/billing.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/billing.tsx @@ -8,6 +8,7 @@ import useSWR from "swr"; import workspaceService from "services/workspace.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { SettingsHeader } from "components/workspace"; // ui import { SecondaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -34,37 +35,40 @@ const BillingSettings: NextPage = () => { title={`${activeWorkspace?.name ?? "Workspace"}`} link={`/${workspaceSlug}`} /> - + } > -
-
-

Billing & Plans

-

[Free launch preview] plan Pro

-
-
+
+ +
-
-

Payment due

-

--

+

Billing & Plans

+

[Free launch preview] plan Pro

+
+
+
+
+

Payment due

+

--

+
+
+
+

Current plan

+

+ You are currently using the free plan +

+ + View Plans and Upgrade + +
+
+

Billing history

+

There are no invoices to display

-
-

Current plan

-

- You are currently using the free plan -

- - View Plans and Upgrade - -
-
-

Billing history

-

There are no invoices to display

-
-
-
+
+
); }; diff --git a/apps/app/pages/[workspaceSlug]/settings/import-export.tsx b/apps/app/pages/[workspaceSlug]/settings/import-export.tsx index 41ef3177b..821ceee7a 100644 --- a/apps/app/pages/[workspaceSlug]/settings/import-export.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/import-export.tsx @@ -2,6 +2,7 @@ import { useRouter } from "next/router"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { SettingsHeader } from "components/workspace"; // components import IntegrationGuide from "components/integration/guide"; // ui @@ -18,11 +19,14 @@ const ImportExport: NextPage = () => { breadcrumbs={ - + } > - +
+ + +
); }; diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index b0558b131..045e9a893 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -14,9 +14,10 @@ import fileService from "services/file.service"; import useToast from "hooks/use-toast"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import SettingsNavbar from "layouts/settings-navbar"; // components import { ImageUploadModal } from "components/core"; -import { DeleteWorkspaceModal } from "components/workspace"; +import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace"; // ui import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -172,163 +173,167 @@ const WorkspaceSettings: NextPage = () => { }} data={activeWorkspace ?? null} /> - {activeWorkspace ? ( -
-
-
-

Logo

-

- Max file size is 5MB. Supported file types are .jpg and .png. -

-
-
-
- -
- { - setIsImageUploadModalOpen(true); - }} - > - {isImageUploading ? "Uploading..." : "Upload"} - - {activeWorkspace.logo && activeWorkspace.logo !== "" && ( - handleDelete(activeWorkspace.logo)}> - {isImageRemoving ? "Removing..." : "Remove"} - - )} +
+ + {activeWorkspace ? ( +
+
+
+

Logo

+

+ Max file size is 5MB. Supported file types are .jpg and .png. +

+
+
+
+ +
+ { + setIsImageUploadModalOpen(true); + }} + > + {isImageUploading ? "Uploading..." : "Upload"} + + {activeWorkspace.logo && activeWorkspace.logo !== "" && ( + handleDelete(activeWorkspace.logo)}> + {isImageRemoving ? "Removing..." : "Remove"} + + )} +
-
-
-
-

URL

-

Your workspace URL.

+
+
+

URL

+

Your workspace URL.

+
+
+ + + copyTextToClipboard( + `${typeof window !== "undefined" && window.location.origin}/${ + activeWorkspace.slug + }` + ).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Workspace link copied to clipboard.", + }); + }) + } + outline + > + + +
-
- - - copyTextToClipboard( - `${typeof window !== "undefined" && window.location.origin}/${ - activeWorkspace.slug - }` - ).then(() => { - setToastAlert({ - type: "success", - title: "Link Copied!", - message: "Workspace link copied to clipboard.", - }); - }) - } - outline - > - +
+
+

Name

+

Give a name to your workspace.

+
+
+ +
+
+
+
+

Company Size

+

How big is your company?

+
+
+ ( + + {COMPANY_SIZE?.map((item) => ( + + {item.label} + + ))} + + )} + /> +
+
+
+ + {isSubmitting ? "Updating..." : "Update Workspace"}
-
-
-
-

Name

-

Give a name to your workspace.

-
-
- +
+
+

Danger Zone

+

+ The danger zone of the workspace delete page is a critical area that requires + careful consideration and attention. When deleting a workspace, all of the data + and resources within that workspace will be permanently removed and cannot be + recovered. +

+
+
+ setIsOpen(true)} outline> + Delete the workspace + +
-
-
-

Company Size

-

How big is your company?

-
-
- ( - - {COMPANY_SIZE?.map((item) => ( - - {item.label} - - ))} - - )} - /> -
+ ) : ( +
+
-
- - {isSubmitting ? "Updating..." : "Update Workspace"} - -
-
-
-

Danger Zone

-

- The danger zone of the workspace delete page is a critical area that requires - careful consideration and attention. When deleting a workspace, all of the data and - resources within that workspace will be permanently removed and cannot be recovered. -

-
-
- setIsOpen(true)} outline> - Delete the workspace - -
-
-
- ) : ( -
- -
- )} + )} +
); }; diff --git a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx index 44717f67d..de3aa1533 100644 --- a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx @@ -9,11 +9,14 @@ import workspaceService from "services/workspace.service"; import IntegrationService from "services/integration"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { SettingsHeader } from "components/workspace"; // components import { SingleIntegrationCard } from "components/integration"; // ui import { Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// icons +import { ExclamationIcon } from "components/icons"; // types import type { NextPage } from "next"; // fetch-keys @@ -44,21 +47,34 @@ const WorkspaceIntegrations: NextPage = () => { } > -
-

Integrations

-
- {appIntegrations ? ( - appIntegrations.map((integration) => ( - - )) - ) : ( - - - - - )} -
-
+
+ +
+
+

Integrations

+
+ +

+ Integrations and importers are only available on the cloud version. We plan to + open-source our SDKs in the near future so that the community can request or + contribute integrations as needed. +

+
+
+
+ {appIntegrations ? ( + appIntegrations.map((integration) => ( + + )) + ) : ( + + + + + )} +
+
+
); }; diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx index 98d4eaaf2..05ae6249d 100644 --- a/apps/app/pages/[workspaceSlug]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx @@ -11,6 +11,7 @@ import useToast from "hooks/use-toast"; import workspaceService from "services/workspace.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { SettingsHeader } from "components/workspace"; // components import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; @@ -137,117 +138,120 @@ const MembersSettings: NextPage = () => { } > -
-
-

Members

- -
- {!workspaceMembers || !workspaceInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
-
- {member.avatar && member.avatar !== "" ? ( - {member.first_name} - ) : member.first_name !== "" ? ( - member.first_name.charAt(0) - ) : ( - member.email.charAt(0) - )} -
-
-

- {member.first_name} {member.last_name} -

-

{member.email}

-
-
-
- {!member?.status && ( -
-

Pending

-
- )} - { - workspaceService - .updateWorkspaceMember(activeWorkspace?.slug as string, member.id, { - role: value, - }) - .then(() => { - mutateMembers( - (prevData) => - prevData?.map((m) => - m.id === member.id ? { ...m, role: value } : m - ), - false - ); - setToastAlert({ - title: "Success", - type: "success", - message: "Member role updated successfully.", - }); - }) - .catch(() => { - setToastAlert({ - title: "Error", - type: "error", - message: "An error occurred while updating member role.", - }); - }); - }} - position="right" - > - {Object.keys(ROLE).map((key) => ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ))} - - - { - if (member.member) { - setSelectedRemoveMember(member.id); - } else { - setSelectedInviteRemoveMember(member.id); - } - }} - > - Remove member - - -
-
- )) - : null} +
+ +
+
+

Members

+
- )} -
+ {!workspaceMembers || !workspaceInvitations ? ( + + + + + + + ) : ( +
+ {members.length > 0 + ? members.map((member) => ( +
+
+
+ {member.avatar && member.avatar !== "" ? ( + {member.first_name} + ) : member.first_name !== "" ? ( + member.first_name.charAt(0) + ) : ( + member.email.charAt(0) + )} +
+
+

+ {member.first_name} {member.last_name} +

+

{member.email}

+
+
+
+ {!member?.status && ( +
+

Pending

+
+ )} + { + workspaceService + .updateWorkspaceMember(activeWorkspace?.slug as string, member.id, { + role: value, + }) + .then(() => { + mutateMembers( + (prevData) => + prevData?.map((m) => + m.id === member.id ? { ...m, role: value } : m + ), + false + ); + setToastAlert({ + title: "Success", + type: "success", + message: "Member role updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + title: "Error", + type: "error", + message: "An error occurred while updating member role.", + }); + }); + }} + position="right" + > + {Object.keys(ROLE).map((key) => ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ))} + + + { + if (member.member) { + setSelectedRemoveMember(member.id); + } else { + setSelectedInviteRemoveMember(member.id); + } + }} + > + Remove member + + +
+
+ )) + : null} +
+ )} +
+
); diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 6f372b4e3..0f50b3b8c 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -8,6 +8,7 @@ import "styles/globals.css"; import "styles/editor.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; +import "styles/react-datepicker.css"; // router import Router from "next/router"; diff --git a/apps/app/pages/signin.tsx b/apps/app/pages/signin.tsx index 3dee9a9e6..ee2469b86 100644 --- a/apps/app/pages/signin.tsx +++ b/apps/app/pages/signin.tsx @@ -1,8 +1,6 @@ import React, { useCallback, useState } from "react"; - import { useRouter } from "next/router"; import Image from "next/image"; - // hooks import useUser from "hooks/use-user"; import useToast from "hooks/use-toast"; diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts index a625c0b37..958205005 100644 --- a/apps/app/services/api.service.ts +++ b/apps/app/services/api.service.ts @@ -1,6 +1,21 @@ import axios from "axios"; import Cookies from "js-cookie"; +const unAuthorizedStatus = [401]; +axios.interceptors.response.use( + (response) => response, + (error) => { + const { status }: any = error.response; + if (unAuthorizedStatus.includes(status)) { + Cookies.remove("refreshToken", { path: "/" }); + Cookies.remove("accessToken", { path: "/" }); + console.log("window.location.href", window.location.pathname); + if (window.location.pathname != "/signin") window.location.href = "/signin"; + } + return Promise.reject(error); + } +); + abstract class APIService { protected baseURL: string; protected headers: any = {}; diff --git a/apps/app/services/estimates.service.ts b/apps/app/services/estimates.service.ts index 0718f286b..d64f37f28 100644 --- a/apps/app/services/estimates.service.ts +++ b/apps/app/services/estimates.service.ts @@ -1,10 +1,14 @@ // services import APIService from "services/api.service"; // types -import type { IEstimate, IEstimateFormData, IEstimatePoint } from "types"; +import type { IEstimate, IEstimateFormData } from "types"; +import trackEventServices from "services/track-event.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; +const trackEvent = + process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; + class ProjectEstimateServices extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); @@ -16,7 +20,11 @@ class ProjectEstimateServices extends APIService { data: IEstimateFormData ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) - .then((response) => response?.data) + .then((response) => { + if (trackEvent) + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE"); + return response?.data; + }) .catch((error) => { throw error?.response; }); @@ -32,7 +40,11 @@ class ProjectEstimateServices extends APIService { `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, data ) - .then((response) => response?.data) + .then((response) => { + if (trackEvent) + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE"); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); @@ -64,7 +76,11 @@ class ProjectEstimateServices extends APIService { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` ) - .then((response) => response?.data) + .then((response) => { + if (trackEvent) + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE"); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); diff --git a/apps/app/services/integration/github.service.ts b/apps/app/services/integration/github.service.ts index 641d2cd2c..101e7ac67 100644 --- a/apps/app/services/integration/github.service.ts +++ b/apps/app/services/integration/github.service.ts @@ -1,10 +1,14 @@ import APIService from "services/api.service"; +import trackEventServices from "services/track-event.service"; + import { IGithubRepoInfo, IGithubServiceImportFormData } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; -const integrationServiceType: string = "github"; +const trackEvent = + process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; +const integrationServiceType: string = "github"; class GithubIntegrationService extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); @@ -41,7 +45,11 @@ class GithubIntegrationService extends APIService { `/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`, data ) - .then((response) => response?.data) + .then((response) => { + if (trackEvent) + trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE"); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); diff --git a/apps/app/services/integration/index.ts b/apps/app/services/integration/index.ts index 425087085..51ecd33c1 100644 --- a/apps/app/services/integration/index.ts +++ b/apps/app/services/integration/index.ts @@ -1,9 +1,14 @@ import APIService from "services/api.service"; +import trackEventServices from "services/track-event.service"; + // types import { IAppIntegration, IImporterService, IWorkspaceIntegration } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; +const trackEvent = + process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; + class IntegrationService extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); @@ -49,7 +54,12 @@ class IntegrationService extends APIService { importerId: string ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`) - .then((res) => res?.data) + .then((response) => { + const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE"; + + if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); diff --git a/apps/app/services/integration/jira.service.ts b/apps/app/services/integration/jira.service.ts index 20ad8166a..456530308 100644 --- a/apps/app/services/integration/jira.service.ts +++ b/apps/app/services/integration/jira.service.ts @@ -1,10 +1,14 @@ import APIService from "services/api.service"; +import trackEventServices from "services/track-event.service"; // types import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; +const trackEvent = + process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; + class JiraImportedService extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); @@ -22,7 +26,11 @@ class JiraImportedService extends APIService { async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data) - .then((response) => response?.data) + .then((response) => { + if (trackEvent) + trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE"); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 5d7ed32f1..54de7119b 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -7,6 +7,7 @@ const trackEvent = // types import type { ICycle, + IEstimate, IGptResponse, IIssue, IIssueComment, @@ -45,7 +46,10 @@ type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE"; type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE"; -type IssueCommentType = "ISSUE_COMMENT_CREATE" | "ISSUE_COMMENT_UPDATE" | "ISSUE_COMMENT_DELETE"; +type IssueCommentEventType = + | "ISSUE_COMMENT_CREATE" + | "ISSUE_COMMENT_UPDATE" + | "ISSUE_COMMENT_DELETE"; export type MiscellaneousEventType = | "TOGGLE_CYCLE_ON" @@ -73,6 +77,13 @@ type IssueLabelEventType = "ISSUE_LABEL_CREATE" | "ISSUE_LABEL_UPDATE" | "ISSUE_ type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_IN_PAGE_BLOCK"; +type IssueEstimateEventType = "ESTIMATE_CREATE" | "ESTIMATE_UPDATE" | "ESTIMATE_DELETE"; + +type ImporterEventType = + | "GITHUB_IMPORTER_CREATE" + | "GITHUB_IMPORTER_DELETE" + | "JIRA_IMPORTER_CREATE" + | "JIRA_IMPORTER_DELETE"; class TrackEventServices extends APIService { constructor() { super("/"); @@ -209,7 +220,7 @@ class TrackEventServices extends APIService { async trackIssueCommentEvent( data: Partial | any, - eventName: IssueCommentType + eventName: IssueCommentEventType ): Promise { let payload: any; if (eventName !== "ISSUE_COMMENT_DELETE") @@ -549,6 +560,61 @@ class TrackEventServices extends APIService { }, }); } + + async trackIssueEstimateEvent( + data: { estimate: IEstimate }, + eventName: IssueEstimateEventType + ): Promise { + let payload: any; + if (eventName === "ESTIMATE_DELETE") payload = data; + else + payload = { + workspaceId: data?.estimate?.workspace_detail?.id, + workspaceName: data?.estimate?.workspace_detail?.name, + workspaceSlug: data?.estimate?.workspace_detail?.slug, + projectId: data?.estimate?.project_detail?.id, + projectName: data?.estimate?.project_detail?.name, + projectIdentifier: data?.estimate?.project_detail?.identifier, + estimateId: data.estimate?.id, + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + }, + }); + } + + async trackImporterEvent(data: any, eventName: ImporterEventType): Promise { + let payload: any; + if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE") + payload = data; + else + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + }, + }); + } } const trackEventServices = new TrackEventServices(); diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css index 8ca808544..4bdd47dbe 100644 --- a/apps/app/styles/globals.css +++ b/apps/app/styles/globals.css @@ -112,6 +112,7 @@ body { .horizontal-scroll-enable::-webkit-scrollbar { display: block; height: 7px; + width: 0; } .horizontal-scroll-enable::-webkit-scrollbar-track { diff --git a/apps/app/styles/react-datepicker.css b/apps/app/styles/react-datepicker.css new file mode 100644 index 000000000..3c5f9a5ca --- /dev/null +++ b/apps/app/styles/react-datepicker.css @@ -0,0 +1,118 @@ +.react-datepicker-wrapper input::placeholder { + color: rgba(var(--color-text-secondary)); + opacity: 1; +} + +.react-datepicker-wrapper input:-ms-input-placeholder { + color: rgba(var(--color-text-secondary)); +} + +.react-datepicker-wrapper .react-datepicker__close-icon::after { + background: transparent; + color: rgba(var(--color-text-secondary)); +} + +.react-datepicker-popper { + z-index: 30 !important; +} + +.react-datepicker-wrapper { + position: relative; + background-color: rgba(var(--color-bg-base)) !important; +} + +.react-datepicker { + font-family: "Inter" !important; + border: none !important; + background-color: rgba(var(--color-bg-base)) !important; +} + +.react-datepicker__month-container { + width: 300px; + background-color: rgba(var(--color-bg-base)) !important; + color: rgba(var(--color-text-base)) !important; + border-radius: 10px !important; + /* border: 1px solid rgba(var(--color-border)) !important; */ +} + +.react-datepicker__header { + border-radius: 10px !important; + background-color: rgba(var(--color-bg-base)) !important; + border: none !important; +} + +.react-datepicker__navigation { + line-height: 0.78; +} + +.react-datepicker__triangle { + border-color: rgba(var(--color-bg-base)) transparent transparent transparent !important; +} + +.react-datepicker__triangle:before { + border-bottom-color: rgba(var(--color-border)) !important; +} +.react-datepicker__triangle:after { + border-bottom-color: rgba(var(--color-bg-base)) !important; +} + +.react-datepicker__current-month { + font-weight: 500 !important; + color: rgba(var(--color-text-base)) !important; +} + +.react-datepicker__month { + border-collapse: collapse; + color: rgba(var(--color-text-base)) !important; +} + +.react-datepicker__day-names { + margin-top: 10px; + margin-left: 14px; + width: 280px; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0; +} + +.react-datepicker__day-name { + color: rgba(var(--color-text-base)) !important; +} + +.react-datepicker__week { + display: grid; + grid-template-columns: repeat(7, 1fr); + margin-left: 8px; +} + +.react-datepicker__day { + color: rgba(var(--color-text-base)) !important; +} + +.react-datepicker__day { + border-radius: 50% !important; + transition: all 0.15s ease-in-out; +} + +.react-datepicker__day:hover { + background-color: rgba(var(--color-bg-surface-2)) !important; + color: rgba(var(--color-text-base)) !important; +} + +.react-datepicker__day--selected { + background-color: #216ba5 !important; + color: white !important; +} + +.react-datepicker__day--today { + font-weight: 800; +} + +.react-datepicker__day--highlighted { + background-color: rgba(var(--color-bg-surface-2)) !important; +} + +.react-datepicker__day--keyboard-selected { + background-color: #216ba5 !important; + color: white !important; +} diff --git a/apps/app/tailwind.config.js b/apps/app/tailwind.config.js index 9dde3bb74..3e2a2da4b 100644 --- a/apps/app/tailwind.config.js +++ b/apps/app/tailwind.config.js @@ -12,10 +12,6 @@ module.exports = { theme: { extend: { colors: { - theme: "#3f76ff", - "hover-gray": "#f5f5f5", - primary: "#f9fafb", // gray-50 - secondary: "white", brand: { accent: withOpacity("--color-accent"), base: withOpacity("--color-bg-base"), diff --git a/apps/app/types/estimate.d.ts b/apps/app/types/estimate.d.ts index 6d14a20ca..32925c793 100644 --- a/apps/app/types/estimate.d.ts +++ b/apps/app/types/estimate.d.ts @@ -8,7 +8,9 @@ export interface IEstimate { updated_by: string; points: IEstimatePoint[]; project: string; + project_detail: IProject; workspace: string; + workspace_detail: IWorkspace; } export interface IEstimatePoint { diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 3f67ef48b..435e47b29 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -35,24 +35,46 @@ services: - redisdata:/data plane-web: container_name: planefrontend - image: makeplane/plane-frontend:0.5-dev + image: makeplane/plane-frontend:0.6 restart: always - command: node apps/app/server.js - env_file: - - ./apps/app/.env + command: [ "/usr/local/bin/start.sh" ] + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_GOOGLE_CLIENTID: 0 + NEXT_PUBLIC_GITHUB_APP_NAME: 0 + NEXT_PUBLIC_GITHUB_ID: 0 + NEXT_PUBLIC_SENTRY_DSN: 0 + NEXT_PUBLIC_ENABLE_OAUTH: 0 + NEXT_PUBLIC_ENABLE_SENTRY: 0 ports: - 3000:3000 plane-api: container_name: planebackend - image: makeplane/plane-backend:0.5-dev + image: makeplane/plane-backend:0.6 build: context: ./apiserver dockerfile: Dockerfile.api restart: always ports: - 8000:8000 - env_file: - - ./apiserver/.env + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} depends_on: - db - redis @@ -62,7 +84,7 @@ services: - redis:redis plane-worker: container_name: planerqworker - image: makeplane/plane-worker:0.5-dev + image: makeplane/plane-worker:0.6 depends_on: - redis - db @@ -71,8 +93,24 @@ services: links: - redis:redis - db:db - env_file: - - ./apiserver/.env + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} volumes: pgdata: redisdata: diff --git a/docker-compose.yml b/docker-compose.yml index 8d05e03cd..e4086acb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,12 +38,21 @@ services: build: context: . dockerfile: ./apps/app/Dockerfile.web - restart: always - command: node apps/app/server.js - env_file: - - ./apps/app/.env + args: + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + command: [ "/usr/local/bin/start.sh" ] ports: - 3000:3000 + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_GOOGLE_CLIENTID: "0" + NEXT_PUBLIC_GITHUB_APP_NAME: "0" + NEXT_PUBLIC_GITHUB_ID: "0" + NEXT_PUBLIC_SENTRY_DSN: "0" + NEXT_PUBLIC_ENABLE_OAUTH: "0" + NEXT_PUBLIC_ENABLE_SENTRY: "0" + NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" + NEXT_PUBLIC_TRACK_EVENTS: "0" plane-api: container_name: planebackend build: @@ -52,8 +61,24 @@ services: restart: always ports: - 8000:8000 - env_file: - - ./apiserver/.env + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} depends_on: - db - redis @@ -74,8 +99,24 @@ services: links: - redis:redis - db:db - env_file: - - ./apiserver/.env + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} volumes: pgdata: redisdata: diff --git a/nginx/supervisor.conf b/nginx/supervisor.conf index db615812e..54b4ca04d 100644 --- a/nginx/supervisor.conf +++ b/nginx/supervisor.conf @@ -2,7 +2,7 @@ nodaemon=true [program:node] -command=node /app/apps/app/server.js +command=sh /usr/local/bin/start.sh autostart=true autorestart=true stderr_logfile=/var/log/node.err.log @@ -21,4 +21,12 @@ command=nginx -g "daemon off;" autostart=true autorestart=true stderr_logfile=/var/log/nginx.err.log -stdout_logfile=/var/log/nginx.out.log \ No newline at end of file +stdout_logfile=/var/log/nginx.out.log + +[program:worker] +directory=/code +command=sh bin/worker +autostart=true +autorestart=true +stderr_logfile=/var/log/worker.err.log +stdout_logfile=/var/log/worker.out.log \ No newline at end of file diff --git a/replace-env-vars.sh b/replace-env-vars.sh new file mode 100644 index 000000000..fe7acc698 --- /dev/null +++ b/replace-env-vars.sh @@ -0,0 +1,17 @@ +#!/bin/sh +FROM=$1 +TO=$2 + +if [ "${FROM}" = "${TO}" ]; then + echo "Nothing to replace, the value is already set to ${TO}." + + exit 0 +fi + +# Only peform action if $FROM and $TO are different. +echo "Replacing all statically built instances of $FROM with this string $TO ." + +find apps/app/.next -type f | +while read file; do + sed -i "s|$FROM|$TO|g" "$file" +done \ No newline at end of file diff --git a/setup.sh b/setup.sh index de95db2f8..e7f9a52dd 100755 --- a/setup.sh +++ b/setup.sh @@ -1,9 +1,7 @@ #!/bin/bash -cp ./apiserver/.env.example ./apiserver/.env -# Generating App environmental variables -cp ./apps/app/.env.example ./apps/app/.env +cp ./.env.example ./.env -echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./apps/app/.env +echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env export LC_ALL=C export LC_CTYPE=C -echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./apiserver/.env +echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env diff --git a/start.sh b/start.sh new file mode 100644 index 000000000..173e333a4 --- /dev/null +++ b/start.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -x + +# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL +# NOTE: if these values are the same, this will be skipped. +/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" + +echo "Starting Plane Frontend.." +node apps/app/server.js \ No newline at end of file diff --git a/turbo.json b/turbo.json index 12476ceb9..1eba6fc9a 100644 --- a/turbo.json +++ b/turbo.json @@ -4,6 +4,7 @@ "NEXT_PUBLIC_GITHUB_ID", "NEXT_PUBLIC_GOOGLE_CLIENTID", "NEXT_PUBLIC_API_BASE_URL", + "API_BASE_URL", "NEXT_PUBLIC_SENTRY_DSN", "SENTRY_AUTH_TOKEN", "NEXT_PUBLIC_SENTRY_ENVIRONMENT", @@ -17,6 +18,7 @@ "NEXT_PUBLIC_CRISP_ID", "NEXT_PUBLIC_ENABLE_SESSION_RECORDER", "NEXT_PUBLIC_SESSION_RECORDER_KEY", + "NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS", "NEXT_PUBLIC_SLACK_CLIENT_ID", "NEXT_PUBLIC_SLACK_CLIENT_SECRET" ], diff --git a/yarn.lock b/yarn.lock index a81d16621..65c491ddc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4229,6 +4229,11 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dotenv@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + ejs@^3.1.6: version "3.1.8" resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz"