diff --git a/.env.example b/.env.example index 9fe0f47d9..082aa753b 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,3 @@ -# Frontend -# Extra image domains that need to be added for Next Image -NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= -# Google Client ID for Google OAuth -NEXT_PUBLIC_GOOGLE_CLIENTID="" -# Github ID for Github OAuth -NEXT_PUBLIC_GITHUB_ID="" -# Github App Name for GitHub Integration -NEXT_PUBLIC_GITHUB_APP_NAME="" -# Sentry DSN for error monitoring -NEXT_PUBLIC_SENTRY_DSN="" -# Enable/Disable OAUTH - default 0 for selfhosted instance -NEXT_PUBLIC_ENABLE_OAUTH=0 -# Enable/Disable sentry -NEXT_PUBLIC_ENABLE_SENTRY=0 -# Enable/Disable session recording -NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 -# Enable/Disable event tracking -NEXT_PUBLIC_TRACK_EVENTS=0 -# Slack for Slack Integration -NEXT_PUBLIC_SLACK_CLIENT_ID="" -# For Telemetry, set it to "app.plane.so" -NEXT_PUBLIC_PLAUSIBLE_DOMAIN="" -# public boards deploy url -NEXT_PUBLIC_DEPLOY_URL="" -# plane deploy using nginx -NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 - -# Backend -# Debug value for api server use it as 0 for production use -DEBUG=0 - -# Error logs -SENTRY_DSN="" - # Database Settings PGUSER="plane" PGPASSWORD="plane" @@ -45,15 +10,6 @@ REDIS_HOST="plane-redis" REDIS_PORT="6379" REDIS_URL="redis://${REDIS_HOST}:6379/" -# Email Settings -EMAIL_HOST="" -EMAIL_HOST_USER="" -EMAIL_HOST_PASSWORD="" -EMAIL_PORT=587 -EMAIL_FROM="Team Plane " -EMAIL_USE_TLS="1" -EMAIL_USE_SSL="0" - # AWS Settings AWS_REGION="" AWS_ACCESS_KEY_ID="access-key" @@ -69,9 +25,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint OPENAI_API_KEY="sk-" # add your openai key here GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access -# Github -GITHUB_CLIENT_SECRET="" # For fetching release notes - # Settings related to Docker DOCKERIZED=1 # set to 1 If using the pre-configured minio setup @@ -80,10 +33,3 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 -# Default Creds -DEFAULT_EMAIL="captain@plane.so" -DEFAULT_PASSWORD="password123" - -# SignUps -ENABLE_SIGNUP="1" -# Auto generated and Required that will be generated from setup.sh diff --git a/apiserver/.env.example b/apiserver/.env.example new file mode 100644 index 000000000..a2a214fe6 --- /dev/null +++ b/apiserver/.env.example @@ -0,0 +1,60 @@ +# Backend +# Debug value for api server use it as 0 for production use +DEBUG=0 + +# Error logs +SENTRY_DSN="" + +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" +DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} + +# Redis Settings +REDIS_HOST="plane-redis" +REDIS_PORT="6379" +REDIS_URL="redis://${REDIS_HOST}:6379/" + +# Email Settings +EMAIL_HOST="" +EMAIL_HOST_USER="" +EMAIL_HOST_PASSWORD="" +EMAIL_PORT=587 +EMAIL_FROM="Team Plane " +EMAIL_USE_TLS="1" +EMAIL_USE_SSL="0" + +# AWS Settings +AWS_REGION="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +# Changing this requires change in the nginx.conf for uploads if using minio setup +AWS_S3_BUCKET_NAME="uploads" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 + +# GPT settings +OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint +OPENAI_API_KEY="sk-" # add your openai key here +GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access + +# Github +GITHUB_CLIENT_SECRET="" # For fetching release notes + +# Settings related to Docker +DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +USE_MINIO=1 + +# Nginx Configuration +NGINX_PORT=80 + +# Default Creds +DEFAULT_EMAIL="captain@plane.so" +DEFAULT_PASSWORD="password123" + +# SignUps +ENABLE_SIGNUP="1" diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 5888b759c..113b54d0e 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -49,6 +49,7 @@ class IssueFlatSerializer(BaseSerializer): "target_date", "sequence_id", "sort_order", + "is_draft", ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 1d4a16eb6..2b83b0b94 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -1038,6 +1038,7 @@ urlpatterns = [ IssueDraftViewSet.as_view( { "get": "list", + "post": "create", } ), name="project-issue-draft", @@ -1047,6 +1048,7 @@ urlpatterns = [ IssueDraftViewSet.as_view( { "get": "retrieve", + "patch": "partial_update", "delete": "destroy", } ), diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index b6dcb88d5..16dce6f47 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -508,7 +508,7 @@ class IssueActivityEndpoint(BaseAPIView): issue_activities = ( IssueActivity.objects.filter(issue_id=issue_id) .filter( - ~Q(field__in=["comment", "vote", "reaction"]), + ~Q(field__in=["comment", "vote", "reaction", "draft"]), project__project_projectmember__member=self.request.user, ) .select_related("actor", "workspace", "issue", "project") @@ -2358,6 +2358,47 @@ class IssueDraftViewSet(BaseViewSet): serializer_class = IssueFlatSerializer model = Issue + + def perform_update(self, serializer): + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) + if current_instance is not None: + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=requested_data, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + ), + ) + + return super().perform_update(serializer) + + + def perform_destroy(self, instance): + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) + if current_instance is not None: + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps( + {"issue_id": str(self.kwargs.get("pk", None))} + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + ), + ) + return super().perform_destroy(instance) + + def get_queryset(self): return ( Issue.objects.annotate( @@ -2383,6 +2424,7 @@ class IssueDraftViewSet(BaseViewSet): ) ) + @method_decorator(gzip_page) def list(self, request, slug, project_id): try: @@ -2492,6 +2534,40 @@ class IssueDraftViewSet(BaseViewSet): ) + def create(self, request, slug, project_id): + try: + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + except Project.DoesNotExist: + return Response( + {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND + ) + + def retrieve(self, request, slug, project_id, pk=None): try: issue = Issue.objects.get( diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 2d13afc35..73fd54a7e 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -396,16 +396,16 @@ def track_assignees( def create_issue_activity( requested_data, current_instance, issue_id, project, actor, issue_activities ): - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"created the issue", - verb="created", - actor=actor, + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"created the issue", + verb="created", + actor=actor, + ) ) - ) def track_estimate_points( @@ -518,11 +518,6 @@ def update_issue_activity( "closed_to": track_closed_to, } - requested_data = json.loads(requested_data) if requested_data is not None else None - current_instance = ( - json.loads(current_instance) if current_instance is not None else None - ) - for key in requested_data: func = ISSUE_ACTIVITY_MAPPER.get(key, None) if func is not None: @@ -1095,6 +1090,69 @@ def delete_issue_relation_activity( ) +def create_draft_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"drafted the issue", + field="draft", + verb="created", + actor=actor, + ) + ) + + +def update_draft_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"created the issue", + verb="updated", + actor=actor, + ) + ) + else: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"updated the draft issue", + field="draft", + verb="updated", + actor=actor, + ) + ) + + + +def delete_draft_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + issue_activities.append( + IssueActivity( + project=project, + workspace=project.workspace, + comment=f"deleted the draft issue", + field="draft", + verb="deleted", + actor=actor, + ) + ) + # Receive message from room group @shared_task def issue_activity( @@ -1166,6 +1224,9 @@ def issue_activity( "comment_reaction.activity.deleted": delete_comment_reaction_activity, "issue_vote.activity.created": create_issue_vote_activity, "issue_vote.activity.deleted": delete_issue_vote_activity, + "issue_draft.activity.created": create_draft_issue_activity, + "issue_draft.activity.updated": update_draft_issue_activity, + "issue_draft.activity.deleted": delete_draft_issue_activity, } func = ACTIVITY_MAPPER.get(type) diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 70762e7b4..9e134042a 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -39,14 +39,90 @@ def group_results(results_data, group_by, sub_group_by=False): for value in results_data: main_group_attribute = resolve_keys(sub_group_by, value) - if str(main_group_attribute) not in main_responsive_dict: - main_responsive_dict[str(main_group_attribute)] = {} group_attribute = resolve_keys(group_by, value) - if str(group_attribute) in main_responsive_dict: - main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list): + if len(main_group_attribute): + for attrib in main_group_attribute: + if str(attrib) not in main_responsive_dict: + main_responsive_dict[str(attrib)] = {} + if str(group_attribute) in main_responsive_dict[str(attrib)]: + main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + else: + main_responsive_dict[str(attrib)][str(group_attribute)] = [] + main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + else: + if str(None) not in main_responsive_dict: + main_responsive_dict[str(None)] = {} + + if str(group_attribute) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(group_attribute)].append(value) + else: + main_responsive_dict[str(None)][str(group_attribute)] = [] + main_responsive_dict[str(None)][str(group_attribute)].append(value) + + elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list): + if str(main_group_attribute) not in main_responsive_dict: + main_responsive_dict[str(main_group_attribute)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + else: + main_responsive_dict[str(main_group_attribute)][str(attrib)] = [] + main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + else: + if str(None) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + else: + main_responsive_dict[str(main_group_attribute)][str(None)] = [] + main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + + elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list): + if len(main_group_attribute): + for main_attrib in main_group_attribute: + if str(main_attrib) not in main_responsive_dict: + main_responsive_dict[str(main_attrib)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(main_attrib)]: + main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + else: + main_responsive_dict[str(main_attrib)][str(attrib)] = [] + main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + else: + if str(None) in main_responsive_dict[str(main_attrib)]: + main_responsive_dict[str(main_attrib)][str(None)].append(value) + else: + main_responsive_dict[str(main_attrib)][str(None)] = [] + main_responsive_dict[str(main_attrib)][str(None)].append(value) + else: + if str(None) not in main_responsive_dict: + main_responsive_dict[str(None)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(attrib)].append(value) + else: + main_responsive_dict[str(None)][str(attrib)] = [] + main_responsive_dict[str(None)][str(attrib)].append(value) + else: + if str(None) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(None)].append(value) + else: + main_responsive_dict[str(None)][str(None)] = [] + main_responsive_dict[str(None)][str(None)].append(value) else: - main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = [] - main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + main_group_attribute = resolve_keys(sub_group_by, value) + group_attribute = resolve_keys(group_by, value) + + if str(main_group_attribute) not in main_responsive_dict: + main_responsive_dict[str(main_group_attribute)] = {} + + if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + else: + main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = [] + main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) return main_responsive_dict diff --git a/docker-compose.yml b/docker-compose.yml index cf631face..e3c1b37be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,5 @@ version: "3.8" -x-api-and-worker-env: &api-and-worker-env - DEBUG: ${DEBUG} - SENTRY_DSN: ${SENTRY_DSN} - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://plane-redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - EMAIL_PORT: ${EMAIL_PORT} - EMAIL_FROM: ${EMAIL_FROM} - EMAIL_USE_TLS: ${EMAIL_USE_TLS} - EMAIL_USE_SSL: ${EMAIL_USE_SSL} - 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} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_BASE: ${OPENAI_API_BASE} - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: ${USE_MINIO} - ENABLE_SIGNUP: ${ENABLE_SIGNUP} - services: plane-web: container_name: planefrontend @@ -40,23 +8,8 @@ services: dockerfile: ./web/Dockerfile.web args: DOCKER_BUILDKIT: 1 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 - NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces restart: always command: /usr/local/bin/start.sh web/server.js web - env_file: - - .env - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_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" depends_on: - plane-api - plane-worker @@ -68,14 +21,8 @@ services: dockerfile: ./space/Dockerfile.space args: DOCKER_BUILDKIT: 1 - NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 restart: always command: /usr/local/bin/start.sh space/server.js space - env_file: - - .env - environment: - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} depends_on: - plane-api - plane-worker @@ -91,9 +38,7 @@ services: restart: always command: ./bin/takeoff env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-db - plane-redis @@ -108,9 +53,7 @@ services: restart: always command: ./bin/worker env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-api - plane-db @@ -126,9 +69,7 @@ services: restart: always command: ./bin/beat env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-api - plane-db @@ -163,8 +104,6 @@ services: command: server /export --console-address ":9090" volumes: - uploads:/export - env_file: - - .env environment: MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} @@ -187,8 +126,6 @@ services: restart: always ports: - ${NGINX_PORT}:80 - env_file: - - .env environment: FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 974f4907d..36a68fa55 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -1,30 +1,29 @@ events { } - http { - sendfile on; + sendfile on; -server { - listen 80; - root /www/data/; - access_log /var/log/nginx/access.log; + server { + listen 80; + root /www/data/; + access_log /var/log/nginx/access.log; - client_max_body_size ${FILE_SIZE_LIMIT}; + client_max_body_size ${FILE_SIZE_LIMIT}; - location / { - proxy_pass http://planefrontend:3000/; + location / { + proxy_pass http://planefrontend:3000/; + } + + location /api/ { + proxy_pass http://planebackend:8000/api/; + } + + location /spaces/ { + proxy_pass http://planedeploy:3000/spaces/; + } + + location /${BUCKET_NAME}/ { + proxy_pass http://plane-minio:9000/uploads/; + } } - - location /api/ { - proxy_pass http://planebackend:8000/api/; - } - - location /spaces/ { - proxy_pass http://planedeploy:3000/spaces/; - } - - location /${BUCKET_NAME}/ { - proxy_pass http://plane-minio:9000/uploads/; - } -} } \ No newline at end of file diff --git a/replace-env-vars.sh b/replace-env-vars.sh deleted file mode 100644 index 949ffd7d7..000000000 --- a/replace-env-vars.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -FROM=$1 -TO=$2 -DIRECTORY=$3 - -if [ "${FROM}" = "${TO}" ]; then - echo "Nothing to replace, the value is already set to ${TO}." - - exit 0 -fi - -# Only perform action if $FROM and $TO are different. -echo "Replacing all statically built instances of $FROM with this string $TO ." - -grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" diff --git a/setup.sh b/setup.sh index 235e1a977..87c0f445b 100755 --- a/setup.sh +++ b/setup.sh @@ -5,15 +5,12 @@ cp ./.env.example ./.env export LC_ALL=C export LC_CTYPE=C - -# Generate the NEXT_PUBLIC_API_BASE_URL with given IP -echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env +cp ./web/.env.example ./web/.env +cp ./space/.env.example ./space/.env +cp ./apiserver/.env.example ./apiserver/.env # Generate the SECRET_KEY that will be used by django -echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env - -# WEB_URL for email redirection and image saving -echo -e "WEB_URL=$1" >> ./.env +echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env # Generate Prompt for taking tiptap auth key echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n" @@ -21,9 +18,7 @@ echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m" echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n" -read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken - +read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken echo "@tiptap-pro:registry=https://registry.tiptap.dev/ -//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc - +//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc \ No newline at end of file diff --git a/space/.env.example b/space/.env.example index 238f70854..56e9f1e95 100644 --- a/space/.env.example +++ b/space/.env.example @@ -1,8 +1,4 @@ -# Base url for the API requests -NEXT_PUBLIC_API_BASE_URL="" -# Public boards deploy URL -NEXT_PUBLIC_DEPLOY_URL="" # Google Client ID for Google OAuth NEXT_PUBLIC_GOOGLE_CLIENTID="" # Flag to toggle OAuth -NEXT_PUBLIC_ENABLE_OAUTH=1 \ No newline at end of file +NEXT_PUBLIC_ENABLE_OAUTH=0 \ No newline at end of file diff --git a/space/Dockerfile.space b/space/Dockerfile.space index 963dad136..12c309134 100644 --- a/space/Dockerfile.space +++ b/space/Dockerfile.space @@ -1,7 +1,6 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat WORKDIR /app -ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -20,19 +19,16 @@ RUN yarn install --network-timeout 500000 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 -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX RUN yarn turbo run build --filter=space -RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space - FROM node:18-alpine AS runner WORKDIR /app @@ -48,14 +44,14 @@ COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./ COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next COPY --from=installer --chown=captain:plane /app/space/public ./space/public -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX + +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX 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 diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index d2f1a1206..d3c29103d 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import Image from "next/image"; import { useRouter } from "next/router"; @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts"; // images -const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : ""; +const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : ""; export const SignInView = observer(() => { const { user: userStore } = useMobxStore(); diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts new file mode 100644 index 000000000..758d7c370 --- /dev/null +++ b/space/helpers/common.helper.ts @@ -0,0 +1 @@ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : ""; diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx index 491146410..12b09641b 100644 --- a/space/pages/onboarding/index.tsx +++ b/space/pages/onboarding/index.tsx @@ -5,7 +5,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { OnBoardingForm } from "components/accounts/onboarding-form"; -const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : ""; +const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : ""; const OnBoardingPage = () => { const { user: userStore } = useMobxStore(); diff --git a/space/services/authentication.service.ts b/space/services/authentication.service.ts index a6f1ec90f..4d861994f 100644 --- a/space/services/authentication.service.ts +++ b/space/services/authentication.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class AuthService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async emailLogin(data: any) { diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 5ef34fc76..d9783d29c 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -1,7 +1,5 @@ -// services import APIService from "services/api.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; interface UnSplashImage { id: string; @@ -29,7 +27,7 @@ interface UnSplashImageUrls { class FileServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async uploadFile(workspaceSlug: string, file: FormData): Promise { diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index 835778fb2..5feb1b00b 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class IssueService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise { diff --git a/space/services/project.service.ts b/space/services/project.service.ts index 291a5f323..0d6eca951 100644 --- a/space/services/project.service.ts +++ b/space/services/project.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class ProjectService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getProjectSettings(workspace_slug: string, project_slug: string): Promise { diff --git a/space/services/user.service.ts b/space/services/user.service.ts index 9a324bb95..21e9f941e 100644 --- a/space/services/user.service.ts +++ b/space/services/user.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class UserService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async currentUser(): Promise { diff --git a/start.sh b/start.sh index dcb97db6d..2685c3826 100644 --- a/start.sh +++ b/start.sh @@ -1,9 +1,5 @@ #!/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" $2 - echo "Starting Plane Frontend.." node $1 diff --git a/web/.env.example b/web/.env.example index 50a6209b2..88a2064c5 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,5 +1,3 @@ -# Base url for the API requests -NEXT_PUBLIC_API_BASE_URL="" # Extra image domains that need to be added for Next Image NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= # Google Client ID for Google OAuth @@ -23,4 +21,4 @@ NEXT_PUBLIC_SLACK_CLIENT_ID="" # For Telemetry, set it to "app.plane.so" NEXT_PUBLIC_PLAUSIBLE_DOMAIN="" # Public boards deploy URL -NEXT_PUBLIC_DEPLOY_URL="" \ No newline at end of file +NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces" \ No newline at end of file diff --git a/web/Dockerfile.web b/web/Dockerfile.web index 40946fa2d..d9260e61d 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -2,7 +2,6 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat # Set working directory WORKDIR /app -ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -14,8 +13,8 @@ FROM node:18-alpine AS installer RUN apk add --no-cache libc6-compat WORKDIR /app -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces +ARG NEXT_PUBLIC_API_BASE_URL="" +ARG NEXT_PUBLIC_DEPLOY_URL="" # First install the dependencies (as they change less often) COPY .gitignore .gitignore @@ -26,18 +25,12 @@ RUN yarn install --network-timeout 500000 # 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 - -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL RUN yarn turbo run build --filter=web -RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} web - FROM node:18-alpine AS runner WORKDIR /app @@ -52,20 +45,15 @@ COPY --from=installer /app/web/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/web/.next/standalone ./ - COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces - -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL +ARG NEXT_PUBLIC_API_BASE_URL="" +ARG NEXT_PUBLIC_DEPLOY_URL="" +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_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 diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 50ab4f904..bb4e72e0c 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -3,8 +3,8 @@ import React, { useState } from "react"; // component import { CustomSelect, ToggleSwitch } from "components/ui"; import { SelectMonthModal } from "components/automation"; -// icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +// icon +import { ArchiveRestore } from "lucide-react"; // constants import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; // types @@ -28,14 +28,18 @@ export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleC handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
-
-
-

Auto-archive closed issues

-

- Plane will automatically archive issues that have been completed or cancelled for the - configured time period. -

+
+
+
+
+ +
+
+

Auto-archive closed issues

+

+ Plane will auto archive issues that have been completed or canceled. +

+
= ({ projectDetails, handleC size="sm" />
- {projectDetails?.archive_in !== 0 && ( -
-
- Auto-archive issues that are closed for -
-
- { - handleChange({ archive_in: val }); - }} - input - verticalPosition="top" - width="w-full" - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - + {projectDetails?.archive_in !== 0 && ( +
+
+
+ Auto-archive issues that are closed for +
+
+ { + handleChange({ archive_in: val }); + }} + input + verticalPosition="bottom" + width="w-full" + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + + +
)} diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index f6cf95f2d..8235c8063 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -5,11 +5,12 @@ import useSWR from "swr"; import { useRouter } from "next/router"; // component -import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui"; +import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui"; import { SelectMonthModal } from "components/automation"; // icons -import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { Squares2X2Icon } from "@heroicons/react/24/outline"; import { StateGroupIcon } from "components/icons"; +import { ArchiveX } from "lucide-react"; // services import stateService from "services/state.service"; // constants @@ -76,14 +77,18 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha handleChange={handleChange} /> -
-
-
-

Auto-close inactive issues

-

- Plane will automatically close the issues that have not been updated for the - configured time period. -

+
+
+
+
+ +
+
+

Auto-close issues

+

+ Plane will automatically close issue that haven’t been completed or canceled. +

+
= ({ projectDetails, handleCha size="sm" />
+ {projectDetails?.close_in !== 0 && ( -
-
-
- Auto-close issues that are inactive for +
+
+
+
+ Auto-close issues that are inactive for +
+
+ { + handleChange({ close_in: val }); + }} + input + width="w-full" + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + +
-
- { - handleChange({ close_in: val }); - }} - input - width="w-full" - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - -
-
-
-
Auto-close Status
-
- - {selectedOption ? ( - - ) : currentDefaultState ? ( - - ) : ( - - )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? ( - State - )} -
- } - onChange={(val: string) => { - handleChange({ default_state: val }); - }} - options={options} - disabled={!multipleOptions} - width="w-full" - input - /> + +
+
Auto-close Status
+
+ + {selectedOption ? ( + + ) : currentDefaultState ? ( + + ) : ( + + )} + {selectedOption?.name + ? selectedOption.name + : currentDefaultState?.name ?? ( + State + )} +
+ } + onChange={(val: string) => { + handleChange({ default_state: val }); + }} + options={options} + disabled={!multipleOptions} + width="w-full" + input + /> +
diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 5f13d960e..957f1131c 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -20,6 +20,7 @@ import fileService from "services/file.service"; import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui"; // hooks import useWorkspaceDetails from "hooks/use-workspace-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; const unsplashEnabled = process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" || @@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC = ({ fileService.getUnsplashImages(1, searchParams) ); + const imagePickerRef = useRef(null); + const { workspaceDetails } = useWorkspaceDetails(); const onDrop = useCallback((acceptedFiles: File[]) => { @@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC = ({ onChange(images[0].urls.regular); }, [value, onChange, images]); + useOutsideClickDetector(imagePickerRef, () => setIsOpen(false)); + if (!unsplashEnabled) return null; return ( setIsOpen((prev) => !prev)} disabled={disabled} > @@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC = ({ leaveTo="transform opacity-0 scale-95" > -
+
diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 7af3bb74f..ab4eb022e 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; // headless ui import { Tab, Transition, Popover } from "@headlessui/react"; // react colors import { TwitterPicker } from "react-color"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { Props } from "./types"; // emojis @@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC = ({ const [recentEmojis, setRecentEmojis] = useState([]); + const emojiPickerRef = useRef(null); + useEffect(() => { setRecentEmojis(getRecentEmojis()); }, []); @@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC = ({ if (!value || value?.length === 0) onChange(getRandomEmoji()); }, [value, onChange]); + useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false)); + return ( = ({ leaveTo="transform opacity-0 scale-95" > -
+
{tabOptions.map((tab) => ( diff --git a/web/components/estimates/single-estimate.tsx b/web/components/estimates/single-estimate.tsx index 3adf986ae..43edfcb2c 100644 --- a/web/components/estimates/single-estimate.tsx +++ b/web/components/estimates/single-estimate.tsx @@ -66,7 +66,7 @@ export const SingleEstimate: React.FC = ({ return ( <> -
+
diff --git a/web/components/icons/index.ts b/web/components/icons/index.ts index ab661a092..bf3e94332 100644 --- a/web/components/icons/index.ts +++ b/web/components/icons/index.ts @@ -84,3 +84,4 @@ export * from "./clock-icon"; export * from "./bell-icon"; export * from "./single-comment-icon"; export * from "./related-icon"; +export * from "./module-icon"; \ No newline at end of file diff --git a/web/components/icons/module-icon.tsx b/web/components/icons/module-icon.tsx new file mode 100644 index 000000000..dbe58eb53 --- /dev/null +++ b/web/components/icons/module-icon.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ModuleIcon: React.FC = ({ + width = "24", + height = "24", + className, + color = "#F15B5B", +}) => ( + + + + + + + +); diff --git a/web/components/integration/github/select-repository.tsx b/web/components/integration/github/select-repository.tsx index 9857c0088..b46942e6d 100644 --- a/web/components/integration/github/select-repository.tsx +++ b/web/components/integration/github/select-repository.tsx @@ -66,6 +66,8 @@ export const SelectRepository: React.FC = ({ content:

{truncateText(repo.full_name, characterLimit)}

, })) ?? []; + if (userRepositories.length < 1) return null; + return ( = ({ integration }) => { {projectIntegration ? ( diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 6306d14ca..61064e777 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -17,7 +17,7 @@ import issuesService from "services/issues.service"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { Component } from "lucide-react"; // types import { IIssueLabels } from "types"; // fetch-keys @@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef( return (
( open ? "text-custom-text-100" : "text-custom-text-200" }`} > - -