Merge branch 'develop' of https://github.com/makeplane/plane into develop

This commit is contained in:
vamsi 2023-05-05 19:46:45 +05:30
commit c16c5b1cb2
99 changed files with 1978 additions and 1402 deletions

View File

@ -1,5 +1,4 @@
# Replace with your instance Public IP # Replace with your instance Public IP
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
NEXT_PUBLIC_GOOGLE_CLIENTID="" NEXT_PUBLIC_GOOGLE_CLIENTID=""
NEXT_PUBLIC_GITHUB_APP_NAME="" NEXT_PUBLIC_GITHUB_APP_NAME=""
@ -10,3 +9,12 @@ NEXT_PUBLIC_ENABLE_SENTRY=0
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
NEXT_PUBLIC_TRACK_EVENTS=0 NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID="" 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=""

View File

@ -1,4 +1,4 @@
name: Build Api Server Docker Image name: Build and Push Backend Docker Image
on: on:
push: push:
@ -10,11 +10,8 @@ on:
jobs: jobs:
build_push_backend: build_push_backend:
name: Build Api Server Docker Image name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps: steps:
- name: Check out the repo - name: Check out the repo
@ -28,20 +25,33 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0 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 uses: docker/login-action@v2.1.0
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Login to Docker Hub
id: meta 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 uses: docker/metadata-action@v4.3.0
with: with:
images: ghcr.io/${{ github.repository }}-backend 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 uses: docker/build-push-action@v4.0.0
with: with:
context: ./apiserver context: ./apiserver
@ -50,5 +60,18 @@ jobs:
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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 }}

View File

@ -1,4 +1,4 @@
name: Build Frontend Docker Image name: Build and Push Frontend Docker Image
on: on:
push: push:
@ -12,9 +12,6 @@ jobs:
build_push_frontend: build_push_frontend:
name: Build Frontend Docker Image name: Build Frontend Docker Image
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
permissions:
contents: read
packages: write
steps: steps:
- name: Check out the repo - name: Check out the repo
@ -35,13 +32,26 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 id: meta
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
with: with:
images: ghcr.io/${{ github.repository }}-frontend 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 uses: docker/build-push-action@v4.0.0
with: with:
context: . context: .
@ -50,5 +60,18 @@ jobs:
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha cache-to: type=gha
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.ghmeta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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 }}

View File

@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo RUN yarn global add turbo
COPY . . COPY . .
@ -16,7 +17,7 @@ FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
WORKDIR /app WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often) # First install the dependencies (as they change less often)
COPY .gitignore .gitignore COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/json/ .
@ -26,9 +27,16 @@ RUN yarn install
# Build the project # Build the project
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json 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 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 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 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"] CMD ["supervisord","-c","/code/supervisor.conf"]

View File

@ -26,7 +26,7 @@
</a> </a>
</p> </p>
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. > 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 > 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 - Run Docker compose up
```bash ```bash
docker-compose up docker-compose -f docker-compose-hub.yml up
``` ```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong> <strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>

View File

@ -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 <team@mailer.plane.so>"
# 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

View File

@ -19,10 +19,29 @@ class CycleSerializer(BaseSerializer):
started_issues = serializers.IntegerField(read_only=True) started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField()
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project") 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: class Meta:
model = Cycle model = Cycle
fields = "__all__" fields = "__all__"

View File

@ -2,9 +2,13 @@
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint from plane.db.models import Estimate, EstimatePoint
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer): class EstimateSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta: class Meta:
model = Estimate model = Estimate
fields = "__all__" fields = "__all__"
@ -27,6 +31,8 @@ class EstimatePointSerializer(BaseSerializer):
class EstimateReadSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True) 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: class Meta:
model = Estimate model = Estimate

View File

@ -2,12 +2,14 @@
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import Importer from plane.db.models import Importer
class ImporterSerializer(BaseSerializer): class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta: class Meta:
model = Importer model = Importer

View File

@ -30,6 +30,7 @@ from plane.db.models import (
CycleFavorite, CycleFavorite,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
User,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -413,10 +414,11 @@ class CycleDateCheckEndpoint(BaseAPIView):
try: try:
start_date = request.data.get("start_date", False) start_date = request.data.get("start_date", False)
end_date = request.data.get("end_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: if not start_date or not end_date:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -428,6 +430,11 @@ class CycleDateCheckEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
) )
if cycle_id:
cycles = cycles.filter(
~Q(pk=cycle_id),
)
if cycles.exists(): if cycles.exists():
return Response( return Response(
{ {
@ -501,6 +508,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
filter=Q(issue_cycle__issue__state__group="backlog"), 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") .order_by("name", "-is_favorite")
) )
@ -545,6 +558,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
filter=Q(issue_cycle__issue__state__group="backlog"), 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") .order_by("name", "-is_favorite")
) )
@ -557,7 +576,7 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
) )
except Exception as e: except Exception as e:
capture_exception(e) print(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -618,6 +637,12 @@ class CompletedCyclesEndpoint(BaseAPIView):
filter=Q(issue_cycle__issue__state__group="backlog"), 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") .order_by("name", "-is_favorite")
) )
@ -693,6 +718,12 @@ class DraftCyclesEndpoint(BaseAPIView):
filter=Q(issue_cycle__issue__state__group="backlog"), 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") .order_by("name", "-is_favorite")
) )

View File

@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
try: try:
estimates = Estimate.objects.filter( estimates = Estimate.objects.filter(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
).prefetch_related("points") ).prefetch_related("points").select_related("workspace", "project")
serializer = EstimateReadSerializer(estimates, many=True) serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -211,7 +211,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
try: try:
EstimatePoint.objects.bulk_update( EstimatePoint.objects.bulk_update(
updated_estimate_points, ["value"], batch_size=10 updated_estimate_points, ["value"], batch_size=10,
) )
except IntegrityError as e: except IntegrityError as e:
return Response( return Response(

View File

@ -39,7 +39,6 @@ class EstimatePoint(ProjectBaseModel):
return f"{self.estimate.name} <{self.key}> <{self.value}>" return f"{self.estimate.name} <{self.key}> <{self.value}>"
class Meta: class Meta:
unique_together = ["value", "estimate"]
verbose_name = "Estimate Point" verbose_name = "Estimate Point"
verbose_name_plural = "Estimate Points" verbose_name_plural = "Estimate Points"
db_table = "estimate_points" db_table = "estimate_points"

View File

@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo RUN yarn global add turbo
COPY . . COPY . .
@ -12,10 +13,10 @@ RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace # Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
WORKDIR /app WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often) # First install the dependencies (as they change less often)
COPY .gitignore .gitignore COPY .gitignore .gitignore
@ -26,9 +27,17 @@ RUN yarn install
# Build the project # Build the project
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json 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 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 FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
@ -43,8 +52,20 @@ COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size # Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing # 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 ./
# 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 ENV NEXT_TELEMETRY_DISABLED 1

View File

@ -821,7 +821,7 @@ export const CommandPalette: React.FC = () => {
> >
<div className="flex items-center gap-2 text-brand-secondary"> <div className="flex items-center gap-2 text-brand-secondary">
<SettingIcon className="h-4 w-4 text-brand-secondary" /> <SettingIcon className="h-4 w-4 text-brand-secondary" />
Billings and Plans Billing and Plans
</div> </div>
</Command.Item> </Command.Item>
<Command.Item <Command.Item

View File

@ -44,7 +44,7 @@ export const AllBoards: React.FC<Props> = ({
return ( return (
<> <>
{groupedByIssues ? ( {groupedByIssues ? (
<div className="horizontal-scroll-enable flex h-[calc(100vh-140px)] gap-x-4"> <div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -392,7 +392,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" /> <LinkIcon className="h-3.5 w-3.5" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
@ -402,7 +402,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>
</Tooltip> </Tooltip>

View File

@ -229,7 +229,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
return calendarIssues ? ( return calendarIssues ? (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<div className="-m-2 h-full overflow-y-auto rounded-lg text-brand-secondary"> <div className="-m-2 h-full overflow-y-auto rounded-lg p-8 text-brand-secondary">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm "> <div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
<Popover className="flex h-full items-center justify-start rounded-lg"> <Popover className="flex h-full items-center justify-start rounded-lg">

View File

@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
return ( return (
<div <div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-brand-base bg-brand-surface-2 p-4 shadow ${ className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-brand-base bg-brand-base p-4 shadow ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
> >
@ -138,7 +138,7 @@ export const GptAssistantModal: React.FC<Props> = ({
</div> </div>
)} )}
{response !== "" && ( {response !== "" && (
<div className="text-sm page-block-section"> <div className="page-block-section text-sm">
Response: Response:
<RemirrorRichTextEditor <RemirrorRichTextEditor
value={`<p>${response}</p>`} value={`<p>${response}</p>`}

View File

@ -134,7 +134,7 @@ export const IssuesFilterView: React.FC = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg"> <Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform rounded-lg border border-brand-base bg-brand-surface-1 p-3 shadow-lg">
<div className="relative divide-y-2 divide-brand-base"> <div className="relative divide-y-2 divide-brand-base">
<div className="space-y-4 pb-3 text-xs"> <div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && ( {issueView !== "calendar" && (

View File

@ -353,7 +353,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e); console.log(e);
}); });
}, },
[workspaceSlug, projectId, cycleId, params] [workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
); );
const removeIssueFromModule = useCallback( const removeIssueFromModule = useCallback(
@ -396,7 +396,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e); console.log(e);
}); });
}, },
[workspaceSlug, projectId, moduleId, params] [workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
); );
const handleTrashBox = useCallback( const handleTrashBox = useCallback(
@ -442,12 +442,9 @@ export const IssuesView: React.FC<Props> = ({
handleClose={() => setTransferIssuesModal(false)} handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal} isOpen={transferIssuesModal}
/> />
{areFiltersApplied && (
<> <>
<div <div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
className={`flex items-center justify-between gap-2 ${
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
}`}
>
<FilterList filters={filters} setFilters={setFilters} /> <FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && ( {areFiltersApplied && (
<PrimaryButton <PrimaryButton
@ -471,10 +468,9 @@ export const IssuesView: React.FC<Props> = ({
</PrimaryButton> </PrimaryButton>
)} )}
</div> </div>
{areFiltersApplied && ( {<div className="mt-3 border-t border-brand-base" />}
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
)}
</> </>
)}
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox"> <StrictModeDroppable droppableId="trashBox">

View File

@ -314,7 +314,7 @@ export const SingleListIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" /> <LinkIcon className="h-3.5 w-3.5" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
@ -324,7 +324,7 @@ export const SingleListIssue: React.FC<Props> = ({
<div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-brand-secondary"> <div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>
</Tooltip> </Tooltip>

View File

@ -65,7 +65,11 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
completedCycles.completed_cycles.length > 0 ? ( completedCycles.completed_cycles.length > 0 ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center gap-2 text-sm text-brand-secondary"> <div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span>Completed cycles are not editable.</span> <span>Completed cycles are not editable.</span>
</div> </div>
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">

View File

@ -1,21 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router";
// react-hook-form // react-hook-form
import { Controller, useForm } from "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 // ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import {
getDateRangeStatus,
isDateGreaterThanToday,
isDateRangeValid,
} from "helpers/date-time.helper";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
@ -34,13 +23,6 @@ const defaultValues: Partial<ICycle> = {
}; };
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => { export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const [isDateValid, setIsDateValid] = useState(true);
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -60,43 +42,6 @@ export const CycleForm: React.FC<Props> = ({ 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(() => { useEffect(() => {
reset({ reset({
...defaultValues, ...defaultValues,
@ -147,30 +92,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
control={control} control={control}
name="start_date" name="start_date"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<DateSelect <DateSelect label="Start date" value={value} onChange={(val) => onChange(val)} />
label="Start date"
value={value}
onChange={(val) => {
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.",
});
}
}
}}
/>
)} )}
/> />
</div> </div>
@ -179,30 +101,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
control={control} control={control}
name="end_date" name="end_date"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<DateSelect <DateSelect label="End date" value={value} onChange={(val) => onChange(val)} />
label="End date"
value={value}
onChange={(val) => {
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.",
});
}
}
}}
/>
)} )}
/> />
</div> </div>
@ -211,18 +110,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
</div> </div>
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-brand-base px-5 pt-5"> <div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-brand-base px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton <PrimaryButton type="submit" loading={isSubmitting}>
type="submit"
className={
checkEmptyDate
? "cursor-pointer"
: isDateValid
? "cursor-pointer"
: "cursor-not-allowed"
}
disabled={checkEmptyDate ? false : isDateValid ? false : true}
loading={isSubmitting}
>
{status {status
? isSubmitting ? isSubmitting
? "Updating Cycle..." ? "Updating Cycle..."

View File

@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
// components // components
import { CycleForm } from "components/cycles"; import { CycleForm } from "components/cycles";
// helper // helper
import { getDateRangeStatus } from "helpers/date-time.helper"; import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.helper";
// types // types
import type { ICycle } from "types"; import type { ICycle } from "types";
// fetch keys // fetch keys
@ -128,6 +128,21 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
}); });
}; };
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<ICycle>) => { const handleFormSubmit = async (formData: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -135,8 +150,63 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
...formData, ...formData,
}; };
if (!data) await createCycle(payload); if (payload.start_date && payload.end_date) {
else await updateCycle(data.id, payload); 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 ( return (

View File

@ -370,7 +370,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</Disclosure.Button> </Disclosure.Button>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span className="text-xs italic text-brand-secondary"> <span className="text-xs italic text-brand-secondary">
{cycleStatus === "upcoming" {cycleStatus === "upcoming"
? "Cycle is yet to start." ? "Cycle is yet to start."
@ -444,7 +448,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</Disclosure.Button> </Disclosure.Button>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span className="text-xs italic text-brand-secondary"> <span className="text-xs italic text-brand-secondary">
No issues found. Please add issue. No issues found. Please add issue.
</span> </span>

View File

@ -148,7 +148,11 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
)) ))
) : ( ) : (
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm"> <div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span className="text-center text-brand-secondary"> <span className="text-center text-brand-secondary">
You dont have any current cycle. Please create one to transfer the You dont have any current cycle. Please create one to transfer the
issues. issues.

View File

@ -39,7 +39,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
return ( return (
<div className="-mt-2 mb-4 flex items-center justify-between"> <div className="-mt-2 mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-brand-secondary"> <div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon height={14} width={14} className="fill-current text-brand-secondary" />
<span>Completed cycles are not editable.</span> <span>Completed cycles are not editable.</span>
</div> </div>

View File

@ -7,10 +7,10 @@ export const ExclamationIcon: React.FC<Props> = ({ width, height, className }) =
width={width} width={width}
height={height} height={height}
className={className} className={className}
viewBox="0 0 14 15" viewBox="0 0 15 15"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M7.55321 11.042C7.70668 11.042 7.83359 10.9918 7.93394 10.8915C8.03428 10.7911 8.08446 10.6642 8.08446 10.5107V7.30553C8.08446 7.16387 8.03133 7.04286 7.92508 6.94251C7.81883 6.84217 7.69487 6.79199 7.55321 6.79199C7.39973 6.79199 7.27283 6.84217 7.17248 6.94251C7.07213 7.04286 7.02196 7.16977 7.02196 7.32324V10.5285C7.02196 10.6701 7.07508 10.7911 7.18133 10.8915C7.28758 10.9918 7.41154 11.042 7.55321 11.042ZM7.50008 5.48158C7.66536 5.48158 7.80407 5.42845 7.91623 5.3222C8.02838 5.21595 8.08446 5.08019 8.08446 4.91491C8.08446 4.74963 8.02838 4.60796 7.91623 4.48991C7.80407 4.37185 7.66536 4.31283 7.50008 4.31283C7.3348 4.31283 7.19609 4.37185 7.08394 4.48991C6.97178 4.60796 6.91571 4.74963 6.91571 4.91491C6.91571 5.08019 6.97178 5.21595 7.08394 5.3222C7.19609 5.42845 7.3348 5.48158 7.50008 5.48158ZM7.50008 14.5837C6.49661 14.5837 5.56397 14.4036 4.70217 14.0436C3.84036 13.6835 3.09071 13.1847 2.45321 12.5472C1.81571 11.9097 1.31692 11.16 0.956852 10.2982C0.596783 9.43644 0.416748 8.5038 0.416748 7.50033C0.416748 6.50866 0.596783 5.58192 0.956852 4.72012C1.31692 3.85831 1.81571 3.10866 2.45321 2.47116C3.09071 1.83366 3.84036 1.33192 4.70217 0.965951C5.56397 0.599978 6.49661 0.416992 7.50008 0.416992C8.49175 0.416992 9.41848 0.599978 10.2803 0.965951C11.1421 1.33192 11.8917 1.83366 12.5292 2.47116C13.1667 3.10866 13.6685 3.85831 14.0345 4.72012C14.4004 5.58192 14.5834 6.50866 14.5834 7.50033C14.5834 8.5038 14.4004 9.43644 14.0345 10.2982C13.6685 11.16 13.1667 11.9097 12.5292 12.5472C11.8917 13.1847 11.1421 13.6835 10.2803 14.0436C9.41848 14.4036 8.49175 14.5837 7.50008 14.5837ZM7.50008 13.5212C9.15286 13.5212 10.5695 12.9309 11.7501 11.7503C12.9306 10.5698 13.5209 9.1531 13.5209 7.50033C13.5209 5.84755 12.9306 4.43088 11.7501 3.25033C10.5695 2.06977 9.15286 1.47949 7.50008 1.47949C5.8473 1.47949 4.43064 2.06977 3.25008 3.25033C2.06953 4.43088 1.47925 5.84755 1.47925 7.50033C1.47925 9.1531 2.06953 10.5698 3.25008 11.7503C4.43064 12.9309 5.8473 13.5212 7.50008 13.5212Z" fill="#858E96"/> <path d="M7.55321 11.042C7.70668 11.042 7.83359 10.9918 7.93394 10.8915C8.03428 10.7911 8.08446 10.6642 8.08446 10.5107V7.30553C8.08446 7.16387 8.03133 7.04286 7.92508 6.94251C7.81883 6.84217 7.69487 6.79199 7.55321 6.79199C7.39973 6.79199 7.27283 6.84217 7.17248 6.94251C7.07213 7.04286 7.02196 7.16977 7.02196 7.32324V10.5285C7.02196 10.6701 7.07508 10.7911 7.18133 10.8915C7.28758 10.9918 7.41154 11.042 7.55321 11.042ZM7.50008 5.48158C7.66536 5.48158 7.80407 5.42845 7.91623 5.3222C8.02838 5.21595 8.08446 5.08019 8.08446 4.91491C8.08446 4.74963 8.02838 4.60796 7.91623 4.48991C7.80407 4.37185 7.66536 4.31283 7.50008 4.31283C7.3348 4.31283 7.19609 4.37185 7.08394 4.48991C6.97178 4.60796 6.91571 4.74963 6.91571 4.91491C6.91571 5.08019 6.97178 5.21595 7.08394 5.3222C7.19609 5.42845 7.3348 5.48158 7.50008 5.48158ZM7.50008 14.5837C6.49661 14.5837 5.56397 14.4036 4.70217 14.0436C3.84036 13.6835 3.09071 13.1847 2.45321 12.5472C1.81571 11.9097 1.31692 11.16 0.956852 10.2982C0.596783 9.43644 0.416748 8.5038 0.416748 7.50033C0.416748 6.50866 0.596783 5.58192 0.956852 4.72012C1.31692 3.85831 1.81571 3.10866 2.45321 2.47116C3.09071 1.83366 3.84036 1.33192 4.70217 0.965951C5.56397 0.599978 6.49661 0.416992 7.50008 0.416992C8.49175 0.416992 9.41848 0.599978 10.2803 0.965951C11.1421 1.33192 11.8917 1.83366 12.5292 2.47116C13.1667 3.10866 13.6685 3.85831 14.0345 4.72012C14.4004 5.58192 14.5834 6.50866 14.5834 7.50033C14.5834 8.5038 14.4004 9.43644 14.0345 10.2982C13.6685 11.16 13.1667 11.9097 12.5292 12.5472C11.8917 13.1847 11.1421 13.6835 10.2803 14.0436C9.41848 14.4036 8.49175 14.5837 7.50008 14.5837ZM7.50008 13.5212C9.15286 13.5212 10.5695 12.9309 11.7501 11.7503C12.9306 10.5698 13.5209 9.1531 13.5209 7.50033C13.5209 5.84755 12.9306 4.43088 11.7501 3.25033C10.5695 2.06977 9.15286 1.47949 7.50008 1.47949C5.8473 1.47949 4.43064 2.06977 3.25008 3.25033C2.06953 4.43088 1.47925 5.84755 1.47925 7.50033C1.47925 9.1531 2.06953 10.5698 3.25008 11.7503C4.43064 12.9309 5.8473 13.5212 7.50008 13.5212Z" />
</svg> </svg>
); );

View File

@ -81,7 +81,7 @@ export const SelectRepository: React.FC<Props> = ({
{userRepositories && options.length < totalCount && ( {userRepositories && options.length < totalCount && (
<button <button
type="button" type="button"
className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray" className="w-full p-1 text-center text-[0.6rem] text-brand-secondary hover:bg-brand-surface-2"
onClick={() => setSize(size + 1)} onClick={() => setSize(size + 1)}
disabled={isValidating} disabled={isValidating}
> >

View File

@ -63,7 +63,11 @@ const IntegrationGuide = () => {
services. This tool will guide you to relocate the issue to Plane. services. This tool will guide you to relocate the issue to Plane.
</div> </div>
</div> </div>
<a href="https://docs.plane.so" target="_blank" rel="noopener noreferrer"> <a
href="https://docs.plane.so/importers/github"
target="_blank"
rel="noopener noreferrer"
>
<div className="flex flex-shrink-0 cursor-pointer items-center gap-2 whitespace-nowrap text-sm font-medium text-[#3F76FF] hover:text-opacity-80"> <div className="flex flex-shrink-0 cursor-pointer items-center gap-2 whitespace-nowrap text-sm font-medium text-[#3F76FF] hover:text-opacity-80">
Read More Read More
<ArrowRightIcon width={"18px"} color={"#3F76FF"} /> <ArrowRightIcon width={"18px"} color={"#3F76FF"} />
@ -124,7 +128,7 @@ const IntegrationGuide = () => {
{importerServices ? ( {importerServices ? (
importerServices.length > 0 ? ( importerServices.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="divide-y"> <div className="divide-y divide-brand-base">
{importerServices.map((service) => ( {importerServices.map((service) => (
<SingleImport <SingleImport
key={service.id} key={service.id}

View File

@ -6,6 +6,8 @@ import { TrashIcon } from "@heroicons/react/24/outline";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types // types
import { IImporterService } from "types"; import { IImporterService } from "types";
// constants
import { IMPORTERS_EXPORTERS_LIST } from "constants/workspace";
type Props = { type Props = {
service: IImporterService; service: IImporterService;
@ -13,17 +15,16 @@ type Props = {
handleDelete: () => void; handleDelete: () => void;
}; };
const importersList: { [key: string]: string } = {
github: "GitHub",
};
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => ( export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 py-3">
<div> <div>
<h4 className="flex items-center gap-2 text-sm"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>
Import from <span className="font-medium">{importersList[service.service]}</span> to{" "} Import from{" "}
<span className="font-medium">{service.project_detail.name}</span> <span className="font-medium">
{IMPORTERS_EXPORTERS_LIST.find((i) => i.provider === service.service)?.title}
</span>{" "}
to <span className="font-medium">{service.project_detail.name}</span>
</span> </span>
<span <span
className={`rounded px-2 py-0.5 text-xs capitalize ${ className={`rounded px-2 py-0.5 text-xs capitalize ${

View File

@ -231,6 +231,16 @@ export const IssueActivitySection: React.FC = () => {
action = `${activityItem.verb} the`; action = `${activityItem.verb} the`;
} else if (activityItem.field === "estimate") { } else if (activityItem.field === "estimate") {
action = "updated the"; 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 // for values that are after the action clause
let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value; let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value;
@ -282,6 +292,18 @@ export const IssueActivitySection: React.FC = () => {
value = "description"; value = "description";
} else if (activityItem.field === "attachment") { } else if (activityItem.field === "attachment") {
value = "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") { } else if (activityItem.field === "link") {
value = "link"; value = "link";
} else if (activityItem.field === "estimate_point") { } else if (activityItem.field === "estimate_point") {

View File

@ -82,7 +82,7 @@ export const IssueAttachments = () => {
} uploaded on ${renderLongDateFormat(file.updated_at)}`} } uploaded on ${renderLongDateFormat(file.updated_at)}`}
> >
<span> <span>
<ExclamationIcon className="h-3 w-3" /> <ExclamationIcon className="h-3 w-3 fill-current text-brand-base" />
</span> </span>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -82,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const isNotAllowed = false; const isNotAllowed = false;
return ( return (
<div className="border-b border-brand-base last:border-b-0 mx-6"> <div className="border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0">
<div key={issue.id} className="flex items-center justify-between gap-2 py-3"> <div key={issue.id} className="flex items-center justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties?.key && ( {properties?.key && (
@ -91,13 +91,13 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
tooltipHeading="Issue ID" tooltipHeading="Issue ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`} tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
> >
<span className="flex-shrink-0 text-xs text-gray-400"> <span className="flex-shrink-0 text-xs text-brand-secondary">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
</Tooltip> </Tooltip>
)} )}
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<span className="break-all text-sm text-brand-base"> <span className="text-[0.825rem] text-brand-base">
{truncateText(issue.name, 50)} {truncateText(issue.name, 50)}
</span> </span>
</Tooltip> </Tooltip>
@ -127,7 +127,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
/> />
)} )}
{properties.sub_issue_count && ( {properties.sub_issue_count && (
<div className="flex items-center gap-1 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm"> <div className="flex items-center gap-1 rounded-md border border-brand-base px-2 py-1 text-xs text-brand-secondary shadow-sm">
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>
)} )}
@ -136,10 +136,10 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
{issue.label_details.map((label) => ( {issue.label_details.map((label) => (
<span <span
key={label.id} key={label.id}
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs" className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary"
> >
<span <span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" className="h-1.5 w-1.5 rounded-full"
style={{ style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000", backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}} }}
@ -171,20 +171,20 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip> </Tooltip>
)} )}
{properties.link && ( {properties.link && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}> <Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-brand-secondary">
<LinkIcon className="h-3.5 w-3.5 text-gray-500" /> <LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
{issue.link_count} {issue.link_count}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
)} )}
{properties.attachment_count && ( {properties.attachment_count && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200"> <div className="flex cursor-default items-center rounded-md border border-brand-base px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}> <Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-brand-secondary">
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" /> <PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
{issue.attachment_count} {issue.attachment_count}
</div> </div>
</Tooltip> </Tooltip>

View File

@ -78,7 +78,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
</span> </span>
</Tooltip> </Tooltip>
} }
value={issueCycle?.cycle_detail.id} value={issueCycle ? issueCycle.cycle_detail.id : null}
onChange={(value: any) => { onChange={(value: any) => {
!value !value
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")

View File

@ -82,7 +82,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({
</span> </span>
</Tooltip> </Tooltip>
} }
value={issueModule?.module_detail?.id} value={issueModule ? issueModule.module_detail?.id : null}
onChange={(value: any) => { onChange={(value: any) => {
!value !value
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") ? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")

View File

@ -416,7 +416,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ issues, module, isOpen,
</Disclosure.Button> </Disclosure.Button>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span className="text-xs italic text-brand-secondary"> <span className="text-xs italic text-brand-secondary">
Invalid date. Please enter valid date. Invalid date. Please enter valid date.
</span> </span>
@ -488,7 +492,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ issues, module, isOpen,
</Disclosure.Button> </Disclosure.Button>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<ExclamationIcon height={14} width={14} /> <ExclamationIcon
height={14}
width={14}
className="fill-current text-brand-secondary"
/>
<span className="text-xs italic text-brand-secondary"> <span className="text-xs italic text-brand-secondary">
No issues found. Please add issue. No issues found. Please add issue.
</span> </span>

View File

@ -44,7 +44,8 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule })
const { setToastAlert } = useToast(); 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 = () => { const handleDeleteModule = () => {
if (!module) return; if (!module) return;

View File

@ -15,8 +15,10 @@ import issuesService from "services/issues.service";
import aiService from "services/ai.service"; import aiService from "services/ai.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
// ui // ui
import { Input, Loader, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
// types // types
import { IPageBlock } from "types"; import { IPageBlock } from "types";
// fetch-keys // fetch-keys
@ -25,9 +27,9 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
handleClose: () => void; handleClose: () => void;
data?: IPageBlock; data?: IPageBlock;
handleAiAssistance?: (response: string) => void;
setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>; setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>;
focus?: keyof IPageBlock; focus?: keyof IPageBlock;
setGptAssistantModal: () => void;
}; };
const defaultValues = { const defaultValues = {
@ -48,11 +50,12 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
export const CreateUpdateBlockInline: React.FC<Props> = ({ export const CreateUpdateBlockInline: React.FC<Props> = ({
handleClose, handleClose,
data, data,
handleAiAssistance,
setIsSyncing, setIsSyncing,
focus, focus,
setGptAssistantModal,
}) => { }) => {
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query; const { workspaceSlug, projectId, pageId } = router.query;
@ -230,6 +233,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
}, [createPageBlock, updatePageBlock, data, handleSubmit]); }, [createPageBlock, updatePageBlock, data, handleSubmit]);
return ( return (
<div className="relative">
<form <form
className="divide-y divide-brand-base rounded-[10px] border border-brand-base shadow" className="divide-y divide-brand-base rounded-[10px] border border-brand-base shadow"
onSubmit={data ? handleSubmit(updatePageBlock) : handleSubmit(createPageBlock)} onSubmit={data ? handleSubmit(updatePageBlock) : handleSubmit(createPageBlock)}
@ -283,19 +287,15 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
</> </>
)} )}
</button> </button>
{data && (
<button <button
type="button" type="button"
className="ml-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-brand-surface-1" className="ml-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-brand-surface-1"
onClick={() => { onClick={() => setGptAssistantModal(true)}
onClose();
setGptAssistantModal();
}}
> >
<SparklesIcon className="h-4 w-4" /> <SparklesIcon className="h-4 w-4" />
AI AI
</button> </button>
)}
</div> </div>
</div> </div>
</div> </div>
@ -312,5 +312,22 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
</PrimaryButton> </PrimaryButton>
</div> </div>
</form> </form>
<GptAssistantModal
block={data ? data : undefined}
isOpen={gptAssistantModal}
handleClose={() => 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")}<p>${response}</p>`);
}
}}
projectId={projectId?.toString() ?? ""}
/>
</div>
); );
}; };

View File

@ -2,12 +2,11 @@ import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import dynamic from "next/dynamic";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// react-beautiful-dnd // react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
// services // services
@ -21,7 +20,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
import { CreateUpdateBlockInline } from "components/pages"; import { CreateUpdateBlockInline } from "components/pages";
// ui // ui
import { CustomMenu, Loader } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
import { LayerDiagonalIcon } from "components/icons"; import { LayerDiagonalIcon } from "components/icons";
import { ArrowPathIcon, LinkIcon } from "@heroicons/react/20/solid"; import { ArrowPathIcon, LinkIcon } from "@heroicons/react/20/solid";
@ -46,15 +45,6 @@ type Props = {
index: number; index: number;
}; };
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader className="mx-4 mt-6">
<Loader.Item height="100px" width="100%" />
</Loader>
),
});
export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index }) => { export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index }) => {
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [createBlockForm, setCreateBlockForm] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false);
@ -291,7 +281,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
{...provided.dragHandleProps} {...provided.dragHandleProps}
> >
<CreateUpdateBlockInline <CreateUpdateBlockInline
setGptAssistantModal={() => setGptAssistantModal((prev) => !prev)} handleAiAssistance={handleAiAssistance}
handleClose={() => setCreateBlockForm(false)} handleClose={() => setCreateBlockForm(false)}
data={block} data={block}
setIsSyncing={setIsSyncing} setIsSyncing={setIsSyncing}

View File

@ -162,7 +162,7 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
} on ${renderLongDateFormat(`${page.created_at}`)}`} } on ${renderLongDateFormat(`${page.created_at}`)}`}
> >
<span> <span>
<ExclamationIcon className="h-4 w-4 text-gray-400" /> <ExclamationIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span> </span>
</Tooltip> </Tooltip>
<CustomMenu verticalEllipsis> <CustomMenu verticalEllipsis>

View File

@ -161,7 +161,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
} on ${renderLongDateFormat(`${page.created_at}`)}`} } on ${renderLongDateFormat(`${page.created_at}`)}`}
> >
<span> <span>
<ExclamationIcon className="h-4 w-4 text-gray-400" /> <ExclamationIcon className="h-4 w-4 fill-current text-brand-secondary" />
</span> </span>
</Tooltip> </Tooltip>

View File

@ -1,6 +1,7 @@
export * from "./create-project-modal"; export * from "./create-project-modal";
export * from "./delete-project-modal"; export * from "./delete-project-modal";
export * from "./sidebar-list"; export * from "./sidebar-list";
export * from "./settings-header"
export * from "./single-integration-card"; export * from "./single-integration-card";
export * from "./single-project-card"; export * from "./single-project-card";
export * from "./single-sidebar-project"; export * from "./single-sidebar-project";

View File

@ -0,0 +1,13 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -172,8 +172,8 @@ export const SingleSidebarProject: React.FC<Props> = ({
<a <a
className={`group flex items-center rounded-md p-2 text-xs font-medium outline-none ${ className={`group flex items-center rounded-md p-2 text-xs font-medium outline-none ${
router.asPath.includes(item.href) router.asPath.includes(item.href)
? "bg-brand-base text-brand-secondary" ? "bg-brand-surface-2 text-brand-base"
: "text-brand-secondary hover:bg-brand-surface-1 hover:text-brand-secondary focus:bg-brand-base focus:text-brand-secondary" : "text-brand-secondary hover:bg-brand-surface-2 hover:text-brand-secondary focus:bg-brand-surface-2 focus:text-brand-secondary"
} ${sidebarCollapse ? "justify-center" : ""}`} } ${sidebarCollapse ? "justify-center" : ""}`}
> >
<div className="grid place-items-center"> <div className="grid place-items-center">

View File

@ -6,5 +6,6 @@ export * from "./help-section";
export * from "./issues-list"; export * from "./issues-list";
export * from "./issues-pie-chart"; export * from "./issues-pie-chart";
export * from "./issues-stats"; export * from "./issues-stats";
export * from "./settings-header";
export * from "./sidebar-dropdown"; export * from "./sidebar-dropdown";
export * from "./sidebar-menu"; export * from "./sidebar-menu";

View File

@ -0,0 +1,13 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Workspace Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the workspace.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -47,8 +47,8 @@ export const WorkspaceSidebarMenu: React.FC = () => {
? router.asPath === link.href ? router.asPath === link.href
: router.asPath.includes(link.href) : router.asPath.includes(link.href)
) )
? "bg-brand-base text-brand-base" ? "bg-brand-surface-2 text-brand-base"
: "text-brand-secondary hover:bg-brand-surface-1 hover:text-brand-secondary focus:bg-brand-base focus:text-brand-secondary" : "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 ${ } group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${
sidebarCollapse ? "justify-center" : "" sidebarCollapse ? "justify-center" : ""
}`} }`}

View File

@ -9,8 +9,8 @@ type Props = {
}; };
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => ( const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => (
<div className="flex w-full flex-row flex-wrap items-center justify-between gap-y-4 border-b border-brand-base bg-brand-sidebar px-5 py-4"> <div className="relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border border-b border-brand-base bg-brand-sidebar px-5 py-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex items-center gap-2">
<div className="block md:hidden"> <div className="block md:hidden">
<button <button
type="button" type="button"

View File

@ -18,13 +18,13 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
const { collapsed: sidebarCollapse } = useTheme(); const { collapsed: sidebarCollapse } = useTheme();
return ( return (
<nav className="relative z-20 h-screen">
<div <div
className={`${sidebarCollapse ? "" : "w-auto md:w-[17rem]"} fixed inset-y-0 top-0 ${ className={`z-20 h-full flex-shrink-0 border-r border-brand-base ${
sidebarCollapse ? "" : "w-auto md:w-[17rem]"
} fixed inset-y-0 top-0 ${
toggleSidebar ? "left-0" : "-left-full md:left-0" toggleSidebar ? "left-0" : "-left-full md:left-0"
} flex h-full flex-col bg-brand-sidebar duration-300 md:relative`} } flex h-full flex-col bg-brand-sidebar duration-300 md:relative`}
> >
<div className="flex h-full flex-1 flex-col border-r border-brand-base">
<div className="flex h-full flex-1 flex-col"> <div className="flex h-full flex-1 flex-col">
<WorkspaceSidebarDropdown /> <WorkspaceSidebarDropdown />
<WorkspaceSidebarMenu /> <WorkspaceSidebarMenu />
@ -32,8 +32,6 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} /> <WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
</div> </div>
</div> </div>
</div>
</nav>
); );
}; };

View File

@ -11,7 +11,6 @@ import useIssuesView from "hooks/use-issues-view";
import Container from "layouts/container"; import Container from "layouts/container";
import AppHeader from "layouts/app-layout/app-header"; import AppHeader from "layouts/app-layout/app-header";
import AppSidebar from "layouts/app-layout/app-sidebar"; import AppSidebar from "layouts/app-layout/app-sidebar";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { NotAuthorizedView, JoinProject } from "components/auth-screens"; import { NotAuthorizedView, JoinProject } from "components/auth-screens";
import { CommandPalette } from "components/command-palette"; import { CommandPalette } from "components/command-palette";
@ -30,7 +29,6 @@ type Meta = {
type Props = { type Props = {
meta?: Meta; meta?: Meta;
children: React.ReactNode; children: React.ReactNode;
noPadding?: boolean;
noHeader?: boolean; noHeader?: boolean;
bg?: "primary" | "secondary"; bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element; breadcrumbs?: JSX.Element;
@ -47,7 +45,6 @@ export const ProjectAuthorizationWrapper: React.FC<Props> = (props) => (
const ProjectAuthorizationWrapped: React.FC<Props> = ({ const ProjectAuthorizationWrapped: React.FC<Props> = ({
meta, meta,
children, children,
noPadding = false,
noHeader = false, noHeader = false,
bg = "primary", bg = "primary",
breadcrumbs, breadcrumbs,
@ -68,8 +65,9 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
return ( return (
<Container meta={meta}> <Container meta={meta}>
<CommandPalette /> <CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden"> <div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} /> <AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{loading ? ( {loading ? (
<div className="grid h-full w-full place-items-center p-4"> <div className="grid h-full w-full place-items-center p-4">
<div className="flex flex-col items-center gap-3 text-center"> <div className="flex flex-col items-center gap-3 text-center">
@ -107,7 +105,15 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
type="project" type="project"
/> />
) : ( ) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto"> <main
className={`relative flex h-full w-full flex-col overflow-hidden ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{!noHeader && ( {!noHeader && (
<AppHeader <AppHeader
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
@ -116,30 +122,11 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
setToggleSidebar={setToggleSidebar} setToggleSidebar={setToggleSidebar}
/> />
)} )}
<div <div className="h-full w-full overflow-hidden">
className={`flex w-full flex-grow flex-col ${ <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
noPadding || issueView === "list" ? "" : settingsLayout ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{settingsLayout && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Project Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
)}
{children} {children}
</div> </div>
</div>
</main> </main>
)} )}
</div> </div>

View File

@ -32,25 +32,21 @@ type Meta = {
type Props = { type Props = {
meta?: Meta; meta?: Meta;
children: React.ReactNode; children: React.ReactNode;
noPadding?: boolean;
noHeader?: boolean; noHeader?: boolean;
bg?: "primary" | "secondary"; bg?: "primary" | "secondary";
breadcrumbs?: JSX.Element; breadcrumbs?: JSX.Element;
left?: JSX.Element; left?: JSX.Element;
right?: JSX.Element; right?: JSX.Element;
profilePage?: boolean;
}; };
export const WorkspaceAuthorizationLayout: React.FC<Props> = ({ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
meta, meta,
children, children,
noPadding = false,
noHeader = false, noHeader = false,
bg = "primary", bg = "primary",
breadcrumbs, breadcrumbs,
left, left,
right, right,
profilePage = false,
}) => { }) => {
const [toggleSidebar, setToggleSidebar] = useState(false); const [toggleSidebar, setToggleSidebar] = useState(false);
@ -101,7 +97,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
<UserAuthorizationLayout> <UserAuthorizationLayout>
<Container meta={meta}> <Container meta={meta}>
<CommandPalette /> <CommandPalette />
<div className="flex h-screen w-full overflow-x-hidden"> <div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} /> <AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
<NotAuthorizedView <NotAuthorizedView
@ -117,7 +113,15 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
type="workspace" type="workspace"
/> />
) : ( ) : (
<main className="flex h-screen w-full min-w-0 flex-col overflow-y-auto"> <main
className={`relative flex h-full w-full flex-col overflow-hidden ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-sidebar"
: "bg-brand-base"
}`}
>
{!noHeader && ( {!noHeader && (
<AppHeader <AppHeader
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
@ -126,34 +130,11 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
setToggleSidebar={setToggleSidebar} setToggleSidebar={setToggleSidebar}
/> />
)} )}
<div <div className="h-full w-full overflow-hidden">
className={`flex w-full flex-grow flex-col ${ <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
noPadding ? "" : settingsLayout || profilePage ? "p-8 lg:px-28" : "p-8"
} ${
bg === "primary"
? "bg-brand-surface-1"
: bg === "secondary"
? "bg-brand-surface-1"
: "bg-brand-base"
}`}
>
{(settingsLayout || profilePage) && (
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">
{profilePage ? "Profile" : "Workspace"} Settings
</h3>
<p className="mt-1 text-brand-secondary">
{profilePage
? "This information will be visible to only you."
: "This information will be displayed to every member of the workspace."}
</p>
</div>
<SettingsNavbar profilePage={profilePage} />
</div>
)}
{children} {children}
</div> </div>
</div>
</main> </main>
)} )}
</div> </div>

View File

@ -1,6 +1,10 @@
require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs"); const { withSentryConfig } = require("@sentry/nextjs");
const path = require("path"); 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 = { const nextConfig = {
reactStrictMode: false, reactStrictMode: false,

View File

@ -23,6 +23,7 @@
"@types/react-datepicker": "^4.8.0", "@types/react-datepicker": "^4.8.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"dotenv": "^16.0.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "12.3.2", "next": "12.3.2",

View File

@ -45,7 +45,7 @@ const WorkspacePage: NextPage = () => {
isOpen={isProductUpdatesModalOpen} isOpen={isProductUpdatesModalOpen}
setIsOpen={setIsProductUpdatesModalOpen} setIsOpen={setIsProductUpdatesModalOpen}
/> />
<div className="h-full w-full"> <div className="p-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div <div
className="text-brand-muted-1 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg bg-brand-base px-8 py-6 md:flex-row md:items-center md:py-3" className="text-brand-muted-1 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg bg-brand-base px-8 py-6 md:flex-row md:items-center md:py-3"

View File

@ -43,7 +43,6 @@ const MyIssuesPage: NextPage = () => {
<BreadcrumbItem title="My Issues" /> <BreadcrumbItem title="My Issues" />
</Breadcrumbs> </Breadcrumbs>
} }
noPadding
right={ right={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && ( {myIssues && myIssues.length > 0 && (
@ -52,7 +51,7 @@ const MyIssuesPage: NextPage = () => {
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${ className={`group flex items-center gap-2 rounded-md border border-brand-base bg-transparent px-3 py-1.5 text-xs hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none ${
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-muted-1" open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
}`} }`}
> >
<span>View</span> <span>View</span>
@ -69,9 +68,8 @@ const MyIssuesPage: NextPage = () => {
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-1/2 z-10 mr-5 mt-1 w-screen max-w-xs translate-x-1/2 transform overflow-hidden rounded-lg bg-brand-base p-3 shadow-lg"> <Popover.Panel className="absolute right-1/2 z-10 mr-5 mt-1 w-screen max-w-xs translate-x-1/2 transform overflow-hidden rounded-lg bg-brand-base p-3 shadow-lg">
<div className="relative flex flex-col gap-1 gap-y-4"> <div className="space-y-2 py-3">
<div className="relative flex flex-col gap-1"> <h4 className="text-sm text-brand-secondary">Properties</h4>
<h4 className="text-base text-gray-600">Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => { {Object.keys(properties).map((key) => {
if (key === "estimate") return null; if (key === "estimate") return null;
@ -80,10 +78,10 @@ const MyIssuesPage: NextPage = () => {
<button <button
key={key} key={key}
type="button" type="button"
className={`rounded border border-theme px-2 py-1 text-xs capitalize ${ className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties] properties[key as keyof Properties]
? "border-theme bg-theme text-white" ? "border-brand-accent bg-brand-accent text-white"
: "" : "border-brand-base"
}`} }`}
onClick={() => setProperties(key as keyof Properties)} onClick={() => setProperties(key as keyof Properties)}
> >
@ -93,7 +91,6 @@ const MyIssuesPage: NextPage = () => {
})} })}
</div> </div>
</div> </div>
</div>
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
</> </>
@ -107,7 +104,7 @@ const MyIssuesPage: NextPage = () => {
document.dispatchEvent(e); document.dispatchEvent(e);
}} }}
> >
<PlusIcon className="w-4 h-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
</PrimaryButton> </PrimaryButton>
</div> </div>
@ -117,24 +114,12 @@ const MyIssuesPage: NextPage = () => {
{myIssues ? ( {myIssues ? (
<> <>
{myIssues.length > 0 ? ( {myIssues.length > 0 ? (
<div className="flex flex-col space-y-5">
<Disclosure as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
<div className="rounded-[10px] border border-brand-base bg-brand-base"> <div>
<div <div className="flex items-center px-4 py-2.5">
className={`flex items-center justify-start bg-brand-surface-1 px-4 py-2.5 ${
open ? "rounded-t-[10px]" : "rounded-[10px]"
}`}
>
<Disclosure.Button> <Disclosure.Button>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${
!open ? "-rotate-90 transform" : ""
}`}
/>
</span>
<h2 className="font-medium leading-5">My Issues</h2> <h2 className="font-medium leading-5">My Issues</h2>
<span className="rounded-full bg-brand-surface-2 py-0.5 px-3 text-sm text-brand-secondary"> <span className="rounded-full bg-brand-surface-2 py-0.5 px-3 text-sm text-brand-secondary">
{myIssues.length} {myIssues.length}
@ -165,7 +150,6 @@ const MyIssuesPage: NextPage = () => {
</div> </div>
)} )}
</Disclosure> </Disclosure>
</div>
) : ( ) : (
<div className="flex h-full w-full flex-col items-center justify-center px-4"> <div className="flex h-full w-full flex-col items-center justify-center px-4">
<EmptySpace <EmptySpace

View File

@ -11,6 +11,7 @@ import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys // fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys"; import { USER_ACTIVITY } from "constants/fetch-keys";
import SettingsNavbar from "layouts/settings-navbar";
const ProfileActivity = () => { const ProfileActivity = () => {
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
@ -25,8 +26,17 @@ const ProfileActivity = () => {
<BreadcrumbItem title="My Profile Activity" /> <BreadcrumbItem title="My Profile Activity" />
</Breadcrumbs> </Breadcrumbs>
} }
profilePage
> >
<div className="px-24 py-8">
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div>
{userActivity ? ( {userActivity ? (
userActivity.results.length > 0 ? ( userActivity.results.length > 0 ? (
<Feeds activities={userActivity.results} /> <Feeds activities={userActivity.results} />
@ -39,6 +49,7 @@ const ProfileActivity = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
)} )}
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -24,6 +24,7 @@ import type { NextPage } from "next";
import type { IUser } from "types"; import type { IUser } from "types";
// constants // constants
import { USER_ROLES } from "constants/workspace"; import { USER_ROLES } from "constants/workspace";
import SettingsNavbar from "layouts/settings-navbar";
const defaultValues: Partial<IUser> = { const defaultValues: Partial<IUser> = {
avatar: "", avatar: "",
@ -130,7 +131,6 @@ const Profile: NextPage = () => {
<BreadcrumbItem title="My Profile" /> <BreadcrumbItem title="My Profile" />
</Breadcrumbs> </Breadcrumbs>
} }
profilePage
> >
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
@ -144,6 +144,16 @@ const Profile: NextPage = () => {
userImage userImage
/> />
{myProfile ? ( {myProfile ? (
<div className="px-24 py-8">
<div className="mb-12 space-y-6">
<div>
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-brand-secondary">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div>
<div className="space-y-8 sm:space-y-12"> <div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
@ -225,7 +235,9 @@ const Profile: NextPage = () => {
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-brand-base">Email</h4> <h4 className="text-lg font-semibold text-brand-base">Email</h4>
<p className="text-sm text-brand-secondary">The email address that you are using.</p> <p className="text-sm text-brand-secondary">
The email address that you are using.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<Input <Input
@ -285,6 +297,7 @@ const Profile: NextPage = () => {
</SecondaryButton> </SecondaryButton>
</div> </div>
</div> </div>
</div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />

View File

@ -124,7 +124,7 @@ const ProjectCycles: NextPage = () => {
handleClose={() => setCreateUpdateCycleModal(false)} handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycle} data={selectedCycle}
/> />
<div className="space-y-8"> <div className="space-y-8 p-8">
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
{currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && ( {currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && (
<h3 className="text-3xl font-semibold text-brand-base">Current Cycle</h3> <h3 className="text-3xl font-semibold text-brand-base">Current Cycle</h3>

View File

@ -122,7 +122,6 @@ const IssueDetailsPage: NextPage = () => {
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
noPadding
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem

View File

@ -77,7 +77,7 @@ const ProjectModules: NextPage = () => {
document.dispatchEvent(e); document.dispatchEvent(e);
}} }}
> >
<PlusIcon className="w-4 h-4" /> <PlusIcon className="h-4 w-4" />
Add Module Add Module
</PrimaryButton> </PrimaryButton>
} }
@ -89,7 +89,7 @@ const ProjectModules: NextPage = () => {
/> />
{modules ? ( {modules ? (
modules.length > 0 ? ( modules.length > 0 ? (
<div className="space-y-5"> <div className="space-y-5 p-8">
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<h3 className="text-3xl font-semibold text-brand-base">Modules</h3> <h3 className="text-3xl font-semibold text-brand-base">Modules</h3>

View File

@ -312,7 +312,7 @@ const SinglePage: NextPage = () => {
} }
> >
{pageDetails ? ( {pageDetails ? (
<div className="h-full w-full space-y-4 rounded-md border border-brand-base bg-brand-base p-4"> <div className="space-y-4 p-4">
<div className="flex items-center justify-between gap-2 px-3"> <div className="flex items-center justify-between gap-2 px-3">
<button <button
type="button" type="button"
@ -543,7 +543,6 @@ const SinglePage: NextPage = () => {
<CreateUpdateBlockInline <CreateUpdateBlockInline
handleClose={() => setCreateBlockForm(false)} handleClose={() => setCreateBlockForm(false)}
focus="name" focus="name"
setGptAssistantModal={() => {}}
/> />
</div> </div>
)} )}

View File

@ -195,7 +195,7 @@ const ProjectPages: NextPage = () => {
</PrimaryButton> </PrimaryButton>
} }
> >
<div className="space-y-4"> <div className="space-y-4 p-8">
<form <form
onSubmit={handleSubmit(createPage)} onSubmit={handleSubmit(createPage)}
className="relative mb-12 flex items-center justify-between gap-2 rounded-[6px] border border-brand-base p-2 shadow" className="relative mb-12 flex items-center justify-between gap-2 rounded-[6px] border border-brand-base p-2 shadow"

View File

@ -20,6 +20,7 @@ import { IProject, IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const defaultValues: Partial<IProject> = { const defaultValues: Partial<IProject> = {
project_lead: null, project_lead: null,
@ -103,7 +104,8 @@ const ControlSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)} className="px-24 py-8">
<SettingsHeader />
<div className="space-y-8 sm:space-y-12"> <div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16"> <div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">

View File

@ -27,6 +27,7 @@ import { IEstimate, IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const EstimatesSettings: NextPage = () => { const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -98,6 +99,14 @@ const EstimatesSettings: NextPage = () => {
return ( return (
<> <>
<CreateUpdateEstimateModal
isOpen={estimateFormOpen}
data={estimateToUpdate}
handleClose={() => {
setEstimateFormOpen(false);
setEstimateToUpdate(undefined);
}}
/>
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
@ -109,14 +118,8 @@ const EstimatesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<CreateUpdateEstimateModal <div className="px-24 py-8">
isOpen={estimateFormOpen} <SettingsHeader />
data={estimateToUpdate}
handleClose={() => {
setEstimateFormOpen(false);
setEstimateToUpdate(undefined);
}}
/>
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
<h3 className="text-2xl font-semibold">Estimates</h3> <h3 className="text-2xl font-semibold">Estimates</h3>
<div className="col-span-12 space-y-5 sm:col-span-7"> <div className="col-span-12 space-y-5 sm:col-span-7">
@ -139,7 +142,7 @@ const EstimatesSettings: NextPage = () => {
</section> </section>
{estimatesList ? ( {estimatesList ? (
estimatesList.length > 0 ? ( estimatesList.length > 0 ? (
<section className="mt-4 mb-8 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6"> <section className="mt-4 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6">
{estimatesList.map((estimate) => ( {estimatesList.map((estimate) => (
<SingleEstimate <SingleEstimate
key={estimate.id} key={estimate.id}
@ -171,6 +174,7 @@ const EstimatesSettings: NextPage = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
)} )}
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

@ -22,6 +22,7 @@ import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const featuresList = [ const featuresList = [
{ {
@ -134,6 +135,8 @@ const FeaturesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8"> <section className="space-y-8">
<h3 className="text-2xl font-semibold">Features</h3> <h3 className="text-2xl font-semibold">Features</h3>
<div className="space-y-5"> <div className="space-y-5">
@ -182,6 +185,7 @@ const FeaturesSettings: NextPage = () => {
</a> </a>
</div> </div>
</section> </section>
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
); );
}; };

View File

@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
// components // components
import { DeleteProjectModal } from "components/project"; import { DeleteProjectModal, SettingsHeader } from "components/project";
import { ImagePickerPopover } from "components/core"; import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker"; import EmojiIconPicker from "components/emoji-icon-picker";
// hooks // hooks
@ -151,7 +151,8 @@ const GeneralSettings: NextPage = () => {
router.push(`/${workspaceSlug}/projects`); router.push(`/${workspaceSlug}/projects`);
}} }}
/> />
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)} className="py-8 px-24">
<SettingsHeader />
<div className="space-y-8 sm:space-y-12"> <div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16"> <div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
@ -222,7 +223,7 @@ const GeneralSettings: NextPage = () => {
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Cover Photo</h4> <h4 className="text-lg font-semibold">Cover Photo</h4>
<p className="text-sm text-gray-500"> <p className="text-sm text-brand-secondary">
Select your cover photo from the given library. Select your cover photo from the given library.
</p> </p>
</div> </div>

View File

@ -10,12 +10,13 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import IntegrationService from "services/integration"; import IntegrationService from "services/integration";
import projectService from "services/project.service"; import projectService from "services/project.service";
// components // components
import { SingleIntegration } from "components/project"; import { SettingsHeader, SingleIntegration } from "components/project";
// ui // ui
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui"; import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline"; import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
import { ExclamationIcon } from "components/icons";
// types // types
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -53,10 +54,26 @@ const ProjectIntegrations: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
{workspaceIntegrations ? ( {workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? ( workspaceIntegrations.length > 0 ? (
<section className="space-y-8"> <section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3> <h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon
height={24}
width={24}
className="fill-current text-brand-base"
/>
<p className="leading-5">
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.
</p>
</div>
</div>
<div className="space-y-5"> <div className="space-y-5">
{workspaceIntegrations.map((integration) => ( {workspaceIntegrations.map((integration) => (
<SingleIntegration <SingleIntegration
@ -91,6 +108,7 @@ const ProjectIntegrations: NextPage = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
)} )}
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
); );
}; };

View File

@ -7,8 +7,6 @@ import useSWR from "swr";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// lib
import { requiredAdmin } from "lib/auth";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
@ -24,10 +22,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssueLabels, UserAuth } from "types"; import { IIssueLabels } from "types";
import type { GetServerSidePropsContext, NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const LabelsSettings: NextPage = () => { const LabelsSettings: NextPage = () => {
// create/edit label form // create/edit label form
@ -103,6 +102,8 @@ const LabelsSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="grid grid-cols-12 gap-10"> <section className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5"> <div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold">Labels</h3> <h3 className="text-2xl font-semibold">Labels</h3>
@ -173,6 +174,7 @@ const LabelsSettings: NextPage = () => {
</> </>
</div> </div>
</section> </section>
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

@ -27,6 +27,7 @@ import type { NextPage } from "next";
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys"; import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
import { SettingsHeader } from "components/project";
const MembersSettings: NextPage = () => { const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
@ -141,6 +142,8 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8"> <section className="space-y-8">
<div className="flex items-end justify-between gap-4"> <div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3> <h3 className="text-2xl font-semibold">Members</h3>
@ -255,6 +258,7 @@ const MembersSettings: NextPage = () => {
</div> </div>
)} )}
</section> </section>
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

@ -28,6 +28,7 @@ import { getStatesList, orderStateGroups } from "helpers/state.helper";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { STATES_LIST } from "constants/fetch-keys"; import { STATES_LIST } from "constants/fetch-keys";
import { SettingsHeader } from "components/project";
const StatesSettings: NextPage = () => { const StatesSettings: NextPage = () => {
const [activeGroup, setActiveGroup] = useState<StateGroup>(null); const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
@ -66,6 +67,8 @@ const StatesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<div className="grid grid-cols-12 gap-10"> <div className="grid grid-cols-12 gap-10">
<div className="col-span-12 sm:col-span-5"> <div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold text-brand-base">States</h3> <h3 className="text-2xl font-semibold text-brand-base">States</h3>
@ -141,6 +144,7 @@ const StatesSettings: NextPage = () => {
)} )}
</div> </div>
</div> </div>
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

@ -97,7 +97,7 @@ const ProjectViews: NextPage = () => {
/> />
{views ? ( {views ? (
views.length > 0 ? ( views.length > 0 ? (
<div className="space-y-5"> <div className="space-y-5 p-8">
<h3 className="text-3xl font-semibold text-brand-base">Views</h3> <h3 className="text-3xl font-semibold text-brand-base">Views</h3>
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base"> <div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
{views.map((view) => ( {views.map((view) => (

View File

@ -83,7 +83,7 @@ const ProjectsPage: NextPage = () => {
data={projects?.find((item) => item.id === deleteProject) ?? null} data={projects?.find((item) => item.id === deleteProject) ?? null}
/> />
{projects ? ( {projects ? (
<> <div className="p-8">
{projects.length === 0 ? ( {projects.length === 0 ? (
<EmptyState <EmptyState
type="project" type="project"
@ -103,7 +103,7 @@ const ProjectsPage: NextPage = () => {
))} ))}
</div> </div>
)} )}
</> </div>
) : ( ) : (
<Loader className="grid grid-cols-3 gap-4"> <Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" /> <Loader.Item height="100px" />

View File

@ -8,6 +8,7 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -34,10 +35,12 @@ const BillingSettings: NextPage = () => {
title={`${activeWorkspace?.name ?? "Workspace"}`} title={`${activeWorkspace?.name ?? "Workspace"}`}
link={`/${workspaceSlug}`} link={`/${workspaceSlug}`}
/> />
<BreadcrumbItem title="Members Settings" /> <BreadcrumbItem title="Billing & Plans Settings" />
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8"> <section className="space-y-8">
<div> <div>
<h3 className="text-3xl font-bold leading-6">Billing & Plans</h3> <h3 className="text-3xl font-bold leading-6">Billing & Plans</h3>
@ -65,6 +68,7 @@ const BillingSettings: NextPage = () => {
</div> </div>
</div> </div>
</section> </section>
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/router";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
// ui // ui
@ -18,11 +19,14 @@ const ImportExport: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} /> <BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} />
<BreadcrumbItem title="Members Settings" /> <BreadcrumbItem title="Import/ Export Settings" />
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<IntegrationGuide /> <IntegrationGuide />
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -14,9 +14,10 @@ import fileService from "services/file.service";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ImageUploadModal } from "components/core"; import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal } from "components/workspace"; import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
// ui // ui
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui"; import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -172,6 +173,8 @@ const WorkspaceSettings: NextPage = () => {
}} }}
data={activeWorkspace ?? null} data={activeWorkspace ?? null}
/> />
<div className="px-24 py-8">
<SettingsHeader />
{activeWorkspace ? ( {activeWorkspace ? (
<div className="space-y-8 sm:space-y-12"> <div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="grid grid-cols-12 gap-4 sm:gap-16">
@ -313,8 +316,9 @@ const WorkspaceSettings: NextPage = () => {
<h4 className="text-lg font-semibold">Danger Zone</h4> <h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-brand-secondary"> <p className="text-sm text-brand-secondary">
The danger zone of the workspace delete page is a critical area that requires 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 careful consideration and attention. When deleting a workspace, all of the data
resources within that workspace will be permanently removed and cannot be recovered. and resources within that workspace will be permanently removed and cannot be
recovered.
</p> </p>
</div> </div>
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
@ -329,6 +333,7 @@ const WorkspaceSettings: NextPage = () => {
<Spinner /> <Spinner />
</div> </div>
)} )}
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -9,11 +9,14 @@ import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration"; import IntegrationService from "services/integration";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import { SingleIntegrationCard } from "components/integration"; import { SingleIntegrationCard } from "components/integration";
// ui // ui
import { Loader } from "components/ui"; import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { ExclamationIcon } from "components/icons";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
@ -44,8 +47,20 @@ const WorkspaceIntegrations: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8"> <section className="space-y-8">
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">Integrations</h3> <h3 className="text-2xl font-semibold">Integrations</h3>
<div className="flex items-center gap-3 rounded-[10px] border border-brand-accent/75 bg-brand-accent/5 p-4 text-sm text-brand-base">
<ExclamationIcon height={24} width={24} className="fill-current text-brand-base" />
<p className="leading-5">
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.
</p>
</div>
</div>
<div className="space-y-5"> <div className="space-y-5">
{appIntegrations ? ( {appIntegrations ? (
appIntegrations.map((integration) => ( appIntegrations.map((integration) => (
@ -59,6 +74,7 @@ const WorkspaceIntegrations: NextPage = () => {
)} )}
</div> </div>
</section> </section>
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -11,6 +11,7 @@ import useToast from "hooks/use-toast";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
@ -137,6 +138,8 @@ const MembersSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="px-24 py-8">
<SettingsHeader />
<section className="space-y-8"> <section className="space-y-8">
<div className="flex items-end justify-between gap-4"> <div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3> <h3 className="text-2xl font-semibold">Members</h3>
@ -248,6 +251,7 @@ const MembersSettings: NextPage = () => {
</div> </div>
)} )}
</section> </section>
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
</> </>
); );

View File

@ -8,6 +8,7 @@ import "styles/globals.css";
import "styles/editor.css"; import "styles/editor.css";
import "styles/command-pallette.css"; import "styles/command-pallette.css";
import "styles/nprogress.css"; import "styles/nprogress.css";
import "styles/react-datepicker.css";
// router // router
import Router from "next/router"; import Router from "next/router";

View File

@ -1,8 +1,6 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Image from "next/image"; import Image from "next/image";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";

View File

@ -1,6 +1,21 @@
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie"; 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 { abstract class APIService {
protected baseURL: string; protected baseURL: string;
protected headers: any = {}; protected headers: any = {};

View File

@ -1,10 +1,14 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
// types // 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 { 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 { class ProjectEstimateServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -16,7 +20,11 @@ class ProjectEstimateServices extends APIService {
data: IEstimateFormData data: IEstimateFormData
): Promise<any> { ): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) 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) => { .catch((error) => {
throw error?.response; throw error?.response;
}); });
@ -32,7 +40,11 @@ class ProjectEstimateServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent)
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE");
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
@ -64,7 +76,11 @@ class ProjectEstimateServices extends APIService {
return this.delete( return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` `/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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,10 +1,14 @@
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
import { IGithubRepoInfo, IGithubServiceImportFormData } from "types"; import { IGithubRepoInfo, IGithubServiceImportFormData } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class GithubIntegrationService extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -41,7 +45,11 @@ class GithubIntegrationService extends APIService {
`/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`, `/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`,
data data
) )
.then((response) => response?.data) .then((response) => {
if (trackEvent)
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE");
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,9 +1,14 @@
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// types // types
import { IAppIntegration, IImporterService, IWorkspaceIntegration } from "types"; import { IAppIntegration, IImporterService, IWorkspaceIntegration } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class IntegrationService extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -49,7 +54,12 @@ class IntegrationService extends APIService {
importerId: string importerId: string
): Promise<any> { ): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -1,10 +1,14 @@
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// types // types
import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types"; import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; 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 { class JiraImportedService extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -22,7 +26,11 @@ class JiraImportedService extends APIService {
async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> { async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data) 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) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -7,6 +7,7 @@ const trackEvent =
// types // types
import type { import type {
ICycle, ICycle,
IEstimate,
IGptResponse, IGptResponse,
IIssue, IIssue,
IIssueComment, IIssueComment,
@ -45,7 +46,10 @@ type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_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 = export type MiscellaneousEventType =
| "TOGGLE_CYCLE_ON" | "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 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 { class TrackEventServices extends APIService {
constructor() { constructor() {
super("/"); super("/");
@ -209,7 +220,7 @@ class TrackEventServices extends APIService {
async trackIssueCommentEvent( async trackIssueCommentEvent(
data: Partial<IIssueComment> | any, data: Partial<IIssueComment> | any,
eventName: IssueCommentType eventName: IssueCommentEventType
): Promise<any> { ): Promise<any> {
let payload: any; let payload: any;
if (eventName !== "ISSUE_COMMENT_DELETE") if (eventName !== "ISSUE_COMMENT_DELETE")
@ -549,6 +560,61 @@ class TrackEventServices extends APIService {
}, },
}); });
} }
async trackIssueEstimateEvent(
data: { estimate: IEstimate },
eventName: IssueEstimateEventType
): Promise<any> {
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<any> {
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(); const trackEventServices = new TrackEventServices();

View File

@ -112,6 +112,7 @@ body {
.horizontal-scroll-enable::-webkit-scrollbar { .horizontal-scroll-enable::-webkit-scrollbar {
display: block; display: block;
height: 7px; height: 7px;
width: 0;
} }
.horizontal-scroll-enable::-webkit-scrollbar-track { .horizontal-scroll-enable::-webkit-scrollbar-track {

View File

@ -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;
}

View File

@ -12,10 +12,6 @@ module.exports = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
theme: "#3f76ff",
"hover-gray": "#f5f5f5",
primary: "#f9fafb", // gray-50
secondary: "white",
brand: { brand: {
accent: withOpacity("--color-accent"), accent: withOpacity("--color-accent"),
base: withOpacity("--color-bg-base"), base: withOpacity("--color-bg-base"),

View File

@ -8,7 +8,9 @@ export interface IEstimate {
updated_by: string; updated_by: string;
points: IEstimatePoint[]; points: IEstimatePoint[];
project: string; project: string;
project_detail: IProject;
workspace: string; workspace: string;
workspace_detail: IWorkspace;
} }
export interface IEstimatePoint { export interface IEstimatePoint {

View File

@ -35,24 +35,46 @@ services:
- redisdata:/data - redisdata:/data
plane-web: plane-web:
container_name: planefrontend container_name: planefrontend
image: makeplane/plane-frontend:0.5-dev image: makeplane/plane-frontend:0.6
restart: always restart: always
command: node apps/app/server.js command: [ "/usr/local/bin/start.sh" ]
env_file: environment:
- ./apps/app/.env 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: ports:
- 3000:3000 - 3000:3000
plane-api: plane-api:
container_name: planebackend container_name: planebackend
image: makeplane/plane-backend:0.5-dev image: makeplane/plane-backend:0.6
build: build:
context: ./apiserver context: ./apiserver
dockerfile: Dockerfile.api dockerfile: Dockerfile.api
restart: always restart: always
ports: ports:
- 8000:8000 - 8000:8000
env_file: environment:
- ./apiserver/.env 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: depends_on:
- db - db
- redis - redis
@ -62,7 +84,7 @@ services:
- redis:redis - redis:redis
plane-worker: plane-worker:
container_name: planerqworker container_name: planerqworker
image: makeplane/plane-worker:0.5-dev image: makeplane/plane-worker:0.6
depends_on: depends_on:
- redis - redis
- db - db
@ -71,8 +93,24 @@ services:
links: links:
- redis:redis - redis:redis
- db:db - db:db
env_file: environment:
- ./apiserver/.env 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: volumes:
pgdata: pgdata:
redisdata: redisdata:

View File

@ -38,12 +38,21 @@ services:
build: build:
context: . context: .
dockerfile: ./apps/app/Dockerfile.web dockerfile: ./apps/app/Dockerfile.web
restart: always args:
command: node apps/app/server.js NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
env_file: command: [ "/usr/local/bin/start.sh" ]
- ./apps/app/.env
ports: ports:
- 3000:3000 - 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: plane-api:
container_name: planebackend container_name: planebackend
build: build:
@ -52,8 +61,24 @@ services:
restart: always restart: always
ports: ports:
- 8000:8000 - 8000:8000
env_file: environment:
- ./apiserver/.env 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: depends_on:
- db - db
- redis - redis
@ -74,8 +99,24 @@ services:
links: links:
- redis:redis - redis:redis
- db:db - db:db
env_file: environment:
- ./apiserver/.env 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: volumes:
pgdata: pgdata:
redisdata: redisdata:

View File

@ -2,7 +2,7 @@
nodaemon=true nodaemon=true
[program:node] [program:node]
command=node /app/apps/app/server.js command=sh /usr/local/bin/start.sh
autostart=true autostart=true
autorestart=true autorestart=true
stderr_logfile=/var/log/node.err.log stderr_logfile=/var/log/node.err.log
@ -22,3 +22,11 @@ autostart=true
autorestart=true autorestart=true
stderr_logfile=/var/log/nginx.err.log stderr_logfile=/var/log/nginx.err.log
stdout_logfile=/var/log/nginx.out.log 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

17
replace-env-vars.sh Normal file
View File

@ -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

View File

@ -1,9 +1,7 @@
#!/bin/bash #!/bin/bash
cp ./apiserver/.env.example ./apiserver/.env cp ./.env.example ./.env
# Generating App environmental variables
cp ./apps/app/.env.example ./apps/app/.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_ALL=C
export LC_CTYPE=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

9
start.sh Normal file
View File

@ -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

View File

@ -4,6 +4,7 @@
"NEXT_PUBLIC_GITHUB_ID", "NEXT_PUBLIC_GITHUB_ID",
"NEXT_PUBLIC_GOOGLE_CLIENTID", "NEXT_PUBLIC_GOOGLE_CLIENTID",
"NEXT_PUBLIC_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL",
"API_BASE_URL",
"NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_AUTH_TOKEN", "SENTRY_AUTH_TOKEN",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT", "NEXT_PUBLIC_SENTRY_ENVIRONMENT",
@ -17,6 +18,7 @@
"NEXT_PUBLIC_CRISP_ID", "NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER", "NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY", "NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_SLACK_CLIENT_ID", "NEXT_PUBLIC_SLACK_CLIENT_ID",
"NEXT_PUBLIC_SLACK_CLIENT_SECRET" "NEXT_PUBLIC_SLACK_CLIENT_SECRET"
], ],

View File

@ -4229,6 +4229,11 @@ dot-case@^3.0.4:
no-case "^3.0.4" no-case "^3.0.4"
tslib "^2.0.3" 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: ejs@^3.1.6:
version "3.1.8" version "3.1.8"
resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz" resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz"