mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of https://github.com/makeplane/plane into develop
This commit is contained in:
commit
c16c5b1cb2
@ -1,5 +1,4 @@
|
||||
# Replace with your instance Public IP
|
||||
# NEXT_PUBLIC_API_BASE_URL = "http://localhost"
|
||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
||||
@ -10,3 +9,12 @@ NEXT_PUBLIC_ENABLE_SENTRY=0
|
||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
||||
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
||||
EMAIL_HOST=""
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=""
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID=""
|
||||
AWS_SECRET_ACCESS_KEY=""
|
||||
AWS_S3_BUCKET_NAME=""
|
||||
OPENAI_API_KEY=""
|
||||
GPT_ENGINE=""
|
45
.github/workflows/push-image-backend.yml
vendored
45
.github/workflows/push-image-backend.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Build Api Server Docker Image
|
||||
name: Build and Push Backend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -10,11 +10,8 @@ on:
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build Api Server Docker Image
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@ -28,20 +25,33 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-backend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: dkrmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-backend
|
||||
|
||||
- name: Build Api Server
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
@ -50,5 +60,18 @@ jobs:
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
|
39
.github/workflows/push-image-frontend.yml
vendored
39
.github/workflows/push-image-frontend.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Build Frontend Docker Image
|
||||
name: Build and Push Frontend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -12,9 +12,6 @@ jobs:
|
||||
build_push_frontend:
|
||||
name: Build Frontend Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@ -35,13 +32,26 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-frontend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-frontend
|
||||
|
||||
- name: Build Frontend Server
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
@ -50,5 +60,18 @@ jobs:
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
|
20
Dockerfile
20
Dockerfile
@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
@ -16,7 +17,7 @@ FROM node:18-alpine AS installer
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
@ -26,9 +27,16 @@ RUN yarn install
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build --filter=app
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
|
||||
@ -108,6 +116,16 @@ COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
COPY nginx/supervisor.conf /code/supervisor.conf
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
USER root
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
|
||||
CMD ["supervisord","-c","/code/supervisor.conf"]
|
||||
|
||||
|
11
README.md
11
README.md
@ -26,7 +26,7 @@
|
||||
</a>
|
||||
</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.
|
||||
@ -58,11 +58,18 @@ cd plane
|
||||
|
||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||
|
||||
- Export Environment Variables
|
||||
|
||||
```bash
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
```
|
||||
|
||||
- Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
docker-compose -f docker-compose-hub.yml up
|
||||
```
|
||||
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
|
@ -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
|
@ -19,10 +19,29 @@ class CycleSerializer(BaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
assignees = serializers.SerializerMethodField()
|
||||
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
def get_assignees(self, obj):
|
||||
members = [
|
||||
{
|
||||
"avatar": assignee.avatar,
|
||||
"first_name": assignee.first_name,
|
||||
"id": assignee.id,
|
||||
}
|
||||
for issue_cycle in obj.issue_cycle.all()
|
||||
for assignee in issue_cycle.issue.assignees.all()
|
||||
]
|
||||
# Use a set comprehension to return only the unique objects
|
||||
unique_objects = {frozenset(item.items()) for item in members}
|
||||
|
||||
# Convert the set back to a list of dictionaries
|
||||
unique_list = [dict(item) for item in unique_objects]
|
||||
|
||||
return unique_list
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
|
@ -2,9 +2,13 @@
|
||||
from .base import BaseSerializer
|
||||
|
||||
from plane.db.models import Estimate, EstimatePoint
|
||||
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
||||
|
||||
|
||||
class EstimateSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
model = Estimate
|
||||
fields = "__all__"
|
||||
@ -27,6 +31,8 @@ class EstimatePointSerializer(BaseSerializer):
|
||||
|
||||
class EstimateReadSerializer(BaseSerializer):
|
||||
points = EstimatePointSerializer(read_only=True, many=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
class Meta:
|
||||
model = Estimate
|
||||
|
@ -2,12 +2,14 @@
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from plane.db.models import Importer
|
||||
|
||||
|
||||
class ImporterSerializer(BaseSerializer):
|
||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Importer
|
||||
|
@ -30,6 +30,7 @@ from plane.db.models import (
|
||||
CycleFavorite,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
User,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
@ -413,10 +414,11 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
try:
|
||||
start_date = request.data.get("start_date", False)
|
||||
end_date = request.data.get("end_date", False)
|
||||
cycle_id = request.data.get("cycle_id", False)
|
||||
|
||||
if not start_date or not end_date:
|
||||
return Response(
|
||||
{"error": "Start date and end date both are required"},
|
||||
{"error": "Start date and end date are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -428,6 +430,11 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
if cycle_id:
|
||||
cycles = cycles.filter(
|
||||
~Q(pk=cycle_id),
|
||||
)
|
||||
|
||||
if cycles.exists():
|
||||
return Response(
|
||||
{
|
||||
@ -501,6 +508,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
|
||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("name", "-is_favorite")
|
||||
)
|
||||
|
||||
@ -545,6 +558,12 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
|
||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("name", "-is_favorite")
|
||||
)
|
||||
|
||||
@ -557,7 +576,7 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@ -618,6 +637,12 @@ class CompletedCyclesEndpoint(BaseAPIView):
|
||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("name", "-is_favorite")
|
||||
)
|
||||
|
||||
@ -693,6 +718,12 @@ class DraftCyclesEndpoint(BaseAPIView):
|
||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("name", "-is_favorite")
|
||||
)
|
||||
|
||||
|
@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
try:
|
||||
estimates = Estimate.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).prefetch_related("points")
|
||||
).prefetch_related("points").select_related("workspace", "project")
|
||||
serializer = EstimateReadSerializer(estimates, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@ -211,7 +211,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
|
||||
try:
|
||||
EstimatePoint.objects.bulk_update(
|
||||
updated_estimate_points, ["value"], batch_size=10
|
||||
updated_estimate_points, ["value"], batch_size=10,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
return Response(
|
||||
|
@ -39,7 +39,6 @@ class EstimatePoint(ProjectBaseModel):
|
||||
return f"{self.estimate.name} <{self.key}> <{self.value}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["value", "estimate"]
|
||||
verbose_name = "Estimate Point"
|
||||
verbose_name_plural = "Estimate Points"
|
||||
db_table = "estimate_points"
|
||||
|
@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
@ -12,10 +13,10 @@ RUN turbo prune --scope=app --docker
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
@ -26,9 +27,17 @@ RUN yarn install
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build --filter=app
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@ -43,8 +52,20 @@ COPY --from=installer /app/apps/app/package.json .
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
USER root
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
USER captain
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
|
@ -821,7 +821,7 @@ export const CommandPalette: React.FC = () => {
|
||||
>
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<SettingIcon className="h-4 w-4 text-brand-secondary" />
|
||||
Billings and Plans
|
||||
Billing and Plans
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
|
@ -44,7 +44,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
{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) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
@ -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">
|
||||
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
|
||||
<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}
|
||||
</div>
|
||||
</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">
|
||||
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
|
||||
<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}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -229,7 +229,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
|
||||
return calendarIssues ? (
|
||||
<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="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">
|
||||
|
@ -121,7 +121,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
@ -138,7 +138,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{response !== "" && (
|
||||
<div className="text-sm page-block-section">
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<RemirrorRichTextEditor
|
||||
value={`<p>${response}</p>`}
|
||||
|
@ -134,7 +134,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
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="space-y-4 pb-3 text-xs">
|
||||
{issueView !== "calendar" && (
|
||||
|
@ -353,7 +353,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, params]
|
||||
[workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
|
||||
);
|
||||
|
||||
const removeIssueFromModule = useCallback(
|
||||
@ -396,7 +396,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, params]
|
||||
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
|
||||
);
|
||||
|
||||
const handleTrashBox = useCallback(
|
||||
@ -442,12 +442,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
{areFiltersApplied && (
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" ? (areFiltersApplied ? "mt-6 px-8" : "") : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{areFiltersApplied && (
|
||||
<PrimaryButton
|
||||
@ -471,10 +468,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
{areFiltersApplied && (
|
||||
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
|
||||
)}
|
||||
{<div className="mt-3 border-t border-brand-base" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
|
@ -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">
|
||||
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
|
||||
<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}
|
||||
</div>
|
||||
</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">
|
||||
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
|
||||
<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}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -65,7 +65,11 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
|
||||
completedCycles.completed_cycles.length > 0 ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<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>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
|
@ -1,21 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// ui
|
||||
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
// helpers
|
||||
import {
|
||||
getDateRangeStatus,
|
||||
isDateGreaterThanToday,
|
||||
isDateRangeValid,
|
||||
} from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
|
||||
@ -34,13 +23,6 @@ const defaultValues: Partial<ICycle> = {
|
||||
};
|
||||
|
||||
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 {
|
||||
register,
|
||||
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(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
@ -147,30 +92,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DateSelect
|
||||
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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DateSelect label="Start date" value={value} onChange={(val) => onChange(val)} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -179,30 +101,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DateSelect
|
||||
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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DateSelect label="End date" value={value} onChange={(val) => onChange(val)} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -211,18 +110,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
</div>
|
||||
<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>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className={
|
||||
checkEmptyDate
|
||||
? "cursor-pointer"
|
||||
: isDateValid
|
||||
? "cursor-pointer"
|
||||
: "cursor-not-allowed"
|
||||
}
|
||||
disabled={checkEmptyDate ? false : isDateValid ? false : true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
|
@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CycleForm } from "components/cycles";
|
||||
// helper
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { ICycle } from "types";
|
||||
// fetch keys
|
||||
@ -128,6 +128,21 @@ export const CreateUpdateCycleModal: React.FC<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>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@ -135,8 +150,63 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
...formData,
|
||||
};
|
||||
|
||||
if (!data) await createCycle(payload);
|
||||
else await updateCycle(data.id, payload);
|
||||
if (payload.start_date && payload.end_date) {
|
||||
if (!isDateGreaterThanToday(payload.end_date)) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Unable to create cycle in past date. Please enter a valid date.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isDateValid = await dateChecker({
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
});
|
||||
|
||||
if (data?.start_date && data?.end_date) {
|
||||
const isDateValidForExistingCycle = await dateChecker({
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
cycle_id: data.id,
|
||||
});
|
||||
|
||||
if (isDateValidForExistingCycle) {
|
||||
await updateCycle(data.id, payload);
|
||||
return;
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDateValid) {
|
||||
if (data) {
|
||||
await updateCycle(data.id, payload);
|
||||
} else {
|
||||
await createCycle(payload);
|
||||
}
|
||||
} else {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (data) {
|
||||
await updateCycle(data.id, payload);
|
||||
} else {
|
||||
await createCycle(payload);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -370,7 +370,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<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">
|
||||
{cycleStatus === "upcoming"
|
||||
? "Cycle is yet to start."
|
||||
@ -444,7 +448,11 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<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">
|
||||
No issues found. Please add issue.
|
||||
</span>
|
||||
|
@ -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">
|
||||
<ExclamationIcon height={14} width={14} />
|
||||
<ExclamationIcon
|
||||
height={14}
|
||||
width={14}
|
||||
className="fill-current text-brand-secondary"
|
||||
/>
|
||||
<span className="text-center text-brand-secondary">
|
||||
You don’t have any current cycle. Please create one to transfer the
|
||||
issues.
|
||||
|
@ -39,7 +39,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
|
||||
return (
|
||||
<div className="-mt-2 mb-4 flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -7,10 +7,10 @@ export const ExclamationIcon: React.FC<Props> = ({ width, height, className }) =
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 14 15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
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>
|
||||
);
|
||||
|
@ -81,7 +81,7 @@ export const SelectRepository: React.FC<Props> = ({
|
||||
{userRepositories && options.length < totalCount && (
|
||||
<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)}
|
||||
disabled={isValidating}
|
||||
>
|
||||
|
@ -63,7 +63,11 @@ const IntegrationGuide = () => {
|
||||
services. This tool will guide you to relocate the issue to Plane.
|
||||
</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">
|
||||
Read More
|
||||
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
|
||||
@ -124,7 +128,7 @@ const IntegrationGuide = () => {
|
||||
{importerServices ? (
|
||||
importerServices.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="divide-y">
|
||||
<div className="divide-y divide-brand-base">
|
||||
{importerServices.map((service) => (
|
||||
<SingleImport
|
||||
key={service.id}
|
||||
|
@ -6,6 +6,8 @@ import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IImporterService } from "types";
|
||||
// constants
|
||||
import { IMPORTERS_EXPORTERS_LIST } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
service: IImporterService;
|
||||
@ -13,17 +15,16 @@ type Props = {
|
||||
handleDelete: () => void;
|
||||
};
|
||||
|
||||
const importersList: { [key: string]: string } = {
|
||||
github: "GitHub",
|
||||
};
|
||||
|
||||
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
|
||||
<div className="flex items-center justify-between gap-2 py-3">
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 text-sm">
|
||||
<span>
|
||||
Import from <span className="font-medium">{importersList[service.service]}</span> to{" "}
|
||||
<span className="font-medium">{service.project_detail.name}</span>
|
||||
Import from{" "}
|
||||
<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
|
||||
className={`rounded px-2 py-0.5 text-xs capitalize ${
|
||||
|
@ -231,6 +231,16 @@ export const IssueActivitySection: React.FC = () => {
|
||||
action = `${activityItem.verb} the`;
|
||||
} else if (activityItem.field === "estimate") {
|
||||
action = "updated the";
|
||||
} else if (activityItem.field === "cycles") {
|
||||
action =
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? "set the cycle to"
|
||||
: "removed the cycle";
|
||||
} else if (activityItem.field === "modules") {
|
||||
action =
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? "set the module to"
|
||||
: "removed the module";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value;
|
||||
@ -282,6 +292,18 @@ export const IssueActivitySection: React.FC = () => {
|
||||
value = "description";
|
||||
} else if (activityItem.field === "attachment") {
|
||||
value = "attachment";
|
||||
} else if (activityItem.field === "cycles") {
|
||||
const cycles =
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? activityItem.new_value
|
||||
: activityItem.old_value;
|
||||
value = cycles ? addSpaceIfCamelCase(cycles) : "None";
|
||||
} else if (activityItem.field === "modules") {
|
||||
const modules =
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? activityItem.new_value
|
||||
: activityItem.old_value;
|
||||
value = modules ? addSpaceIfCamelCase(modules) : "None";
|
||||
} else if (activityItem.field === "link") {
|
||||
value = "link";
|
||||
} else if (activityItem.field === "estimate_point") {
|
||||
|
@ -82,7 +82,7 @@ export const IssueAttachments = () => {
|
||||
} uploaded on ${renderLongDateFormat(file.updated_at)}`}
|
||||
>
|
||||
<span>
|
||||
<ExclamationIcon className="h-3 w-3" />
|
||||
<ExclamationIcon className="h-3 w-3 fill-current text-brand-base" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -82,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
const isNotAllowed = false;
|
||||
|
||||
return (
|
||||
<div className="border-b border-brand-base last:border-b-0 mx-6">
|
||||
<div key={issue.id} className="flex items-center justify-between gap-2 py-3">
|
||||
<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">
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties?.key && (
|
||||
@ -91,13 +91,13 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
tooltipHeading="Issue 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}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<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)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@ -127,7 +127,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
/>
|
||||
)}
|
||||
{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"}
|
||||
</div>
|
||||
)}
|
||||
@ -136,10 +136,10 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
{issue.label_details.map((label) => (
|
||||
<span
|
||||
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
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
@ -171,20 +171,20 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
</Tooltip>
|
||||
)}
|
||||
{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}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
|
||||
<div className="flex items-center gap-1 text-brand-secondary">
|
||||
<LinkIcon className="h-3.5 w-3.5 text-brand-secondary" />
|
||||
{issue.link_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{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}`}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
|
||||
<div className="flex items-center gap-1 text-brand-secondary">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-brand-secondary" />
|
||||
{issue.attachment_count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -78,7 +78,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
value={issueCycle?.cycle_detail.id}
|
||||
value={issueCycle ? issueCycle.cycle_detail.id : null}
|
||||
onChange={(value: any) => {
|
||||
!value
|
||||
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
|
||||
|
@ -82,7 +82,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
value={issueModule?.module_detail?.id}
|
||||
value={issueModule ? issueModule.module_detail?.id : null}
|
||||
onChange={(value: any) => {
|
||||
!value
|
||||
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
|
||||
|
@ -416,7 +416,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ issues, module, isOpen,
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<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">
|
||||
Invalid date. Please enter valid date.
|
||||
</span>
|
||||
@ -488,7 +492,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ issues, module, isOpen,
|
||||
</Disclosure.Button>
|
||||
) : (
|
||||
<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">
|
||||
No issues found. Please add issue.
|
||||
</span>
|
||||
|
@ -44,7 +44,8 @@ export const SingleModuleCard: React.FC<Props> = ({ module, handleEditModule })
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const completionPercentage = (module.completed_issues / module.total_issues) * 100;
|
||||
const completionPercentage =
|
||||
((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
||||
|
||||
const handleDeleteModule = () => {
|
||||
if (!module) return;
|
||||
|
@ -15,8 +15,10 @@ import issuesService from "services/issues.service";
|
||||
import aiService from "services/ai.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GptAssistantModal } from "components/core";
|
||||
// ui
|
||||
import { Input, Loader, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
import { Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import { IPageBlock } from "types";
|
||||
// fetch-keys
|
||||
@ -25,9 +27,9 @@ import { PAGE_BLOCKS_LIST } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
data?: IPageBlock;
|
||||
handleAiAssistance?: (response: string) => void;
|
||||
setIsSyncing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
focus?: keyof IPageBlock;
|
||||
setGptAssistantModal: () => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
@ -48,11 +50,12 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
handleClose,
|
||||
data,
|
||||
handleAiAssistance,
|
||||
setIsSyncing,
|
||||
focus,
|
||||
setGptAssistantModal,
|
||||
}) => {
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
@ -230,6 +233,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
}, [createPageBlock, updatePageBlock, data, handleSubmit]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<form
|
||||
className="divide-y divide-brand-base rounded-[10px] border border-brand-base shadow"
|
||||
onSubmit={data ? handleSubmit(updatePageBlock) : handleSubmit(createPageBlock)}
|
||||
@ -283,19 +287,15 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{data && (
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-brand-surface-1"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setGptAssistantModal();
|
||||
}}
|
||||
onClick={() => setGptAssistantModal(true)}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -312,5 +312,22 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -2,12 +2,11 @@ import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// services
|
||||
@ -21,7 +20,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import { CreateUpdateBlockInline } from "components/pages";
|
||||
// ui
|
||||
import { CustomMenu, Loader } from "components/ui";
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
import { ArrowPathIcon, LinkIcon } from "@heroicons/react/20/solid";
|
||||
@ -46,15 +45,6 @@ type Props = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Loader className="mx-4 mt-6">
|
||||
<Loader.Item height="100px" width="100%" />
|
||||
</Loader>
|
||||
),
|
||||
});
|
||||
|
||||
export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index }) => {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [createBlockForm, setCreateBlockForm] = useState(false);
|
||||
@ -291,7 +281,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, index
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<CreateUpdateBlockInline
|
||||
setGptAssistantModal={() => setGptAssistantModal((prev) => !prev)}
|
||||
handleAiAssistance={handleAiAssistance}
|
||||
handleClose={() => setCreateBlockForm(false)}
|
||||
data={block}
|
||||
setIsSyncing={setIsSyncing}
|
||||
|
@ -162,7 +162,7 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
|
||||
} on ${renderLongDateFormat(`${page.created_at}`)}`}
|
||||
>
|
||||
<span>
|
||||
<ExclamationIcon className="h-4 w-4 text-gray-400" />
|
||||
<ExclamationIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<CustomMenu verticalEllipsis>
|
||||
|
@ -161,7 +161,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
} on ${renderLongDateFormat(`${page.created_at}`)}`}
|
||||
>
|
||||
<span>
|
||||
<ExclamationIcon className="h-4 w-4 text-gray-400" />
|
||||
<ExclamationIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from "./create-project-modal";
|
||||
export * from "./delete-project-modal";
|
||||
export * from "./sidebar-list";
|
||||
export * from "./settings-header"
|
||||
export * from "./single-integration-card";
|
||||
export * from "./single-project-card";
|
||||
export * from "./single-sidebar-project";
|
||||
|
13
apps/app/components/project/settings-header.tsx
Normal file
13
apps/app/components/project/settings-header.tsx
Normal 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>
|
||||
);
|
@ -172,8 +172,8 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
<a
|
||||
className={`group flex items-center rounded-md p-2 text-xs font-medium outline-none ${
|
||||
router.asPath.includes(item.href)
|
||||
? "bg-brand-base text-brand-secondary"
|
||||
: "text-brand-secondary hover:bg-brand-surface-1 hover:text-brand-secondary focus:bg-brand-base focus:text-brand-secondary"
|
||||
? "bg-brand-surface-2 text-brand-base"
|
||||
: "text-brand-secondary hover:bg-brand-surface-2 hover:text-brand-secondary focus:bg-brand-surface-2 focus:text-brand-secondary"
|
||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||
>
|
||||
<div className="grid place-items-center">
|
||||
|
@ -6,5 +6,6 @@ export * from "./help-section";
|
||||
export * from "./issues-list";
|
||||
export * from "./issues-pie-chart";
|
||||
export * from "./issues-stats";
|
||||
export * from "./settings-header";
|
||||
export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu";
|
||||
|
13
apps/app/components/workspace/settings-header.tsx
Normal file
13
apps/app/components/workspace/settings-header.tsx
Normal 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>
|
||||
);
|
@ -47,8 +47,8 @@ export const WorkspaceSidebarMenu: React.FC = () => {
|
||||
? router.asPath === link.href
|
||||
: router.asPath.includes(link.href)
|
||||
)
|
||||
? "bg-brand-base text-brand-base"
|
||||
: "text-brand-secondary hover:bg-brand-surface-1 hover:text-brand-secondary focus:bg-brand-base focus:text-brand-secondary"
|
||||
? "bg-brand-surface-2 text-brand-base"
|
||||
: "text-brand-secondary hover:bg-brand-surface-2 hover:text-brand-secondary focus:bg-brand-surface-2 focus:text-brand-secondary"
|
||||
} group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
|
@ -9,8 +9,8 @@ type Props = {
|
||||
};
|
||||
|
||||
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="flex flex-wrap items-center gap-2">
|
||||
<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 items-center gap-2">
|
||||
<div className="block md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -18,13 +18,13 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
|
||||
const { collapsed: sidebarCollapse } = useTheme();
|
||||
|
||||
return (
|
||||
<nav className="relative z-20 h-screen">
|
||||
<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"
|
||||
} 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">
|
||||
<WorkspaceSidebarDropdown />
|
||||
<WorkspaceSidebarMenu />
|
||||
@ -32,8 +32,6 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
|
||||
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -11,7 +11,6 @@ import useIssuesView from "hooks/use-issues-view";
|
||||
import Container from "layouts/container";
|
||||
import AppHeader from "layouts/app-layout/app-header";
|
||||
import AppSidebar from "layouts/app-layout/app-sidebar";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
// components
|
||||
import { NotAuthorizedView, JoinProject } from "components/auth-screens";
|
||||
import { CommandPalette } from "components/command-palette";
|
||||
@ -30,7 +29,6 @@ type Meta = {
|
||||
type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
noHeader?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
breadcrumbs?: JSX.Element;
|
||||
@ -47,7 +45,6 @@ export const ProjectAuthorizationWrapper: React.FC<Props> = (props) => (
|
||||
const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
meta,
|
||||
children,
|
||||
noPadding = false,
|
||||
noHeader = false,
|
||||
bg = "primary",
|
||||
breadcrumbs,
|
||||
@ -68,8 +65,9 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
return (
|
||||
<Container meta={meta}>
|
||||
<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} />
|
||||
|
||||
{loading ? (
|
||||
<div className="grid h-full w-full place-items-center p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
@ -107,7 +105,15 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
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 && (
|
||||
<AppHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
@ -116,30 +122,11 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex w-full flex-grow flex-col ${
|
||||
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>
|
||||
)}
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
|
@ -32,25 +32,21 @@ type Meta = {
|
||||
type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
noHeader?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
breadcrumbs?: JSX.Element;
|
||||
left?: JSX.Element;
|
||||
right?: JSX.Element;
|
||||
profilePage?: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
meta,
|
||||
children,
|
||||
noPadding = false,
|
||||
noHeader = false,
|
||||
bg = "primary",
|
||||
breadcrumbs,
|
||||
left,
|
||||
right,
|
||||
profilePage = false,
|
||||
}) => {
|
||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
||||
|
||||
@ -101,7 +97,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
<UserAuthorizationLayout>
|
||||
<Container meta={meta}>
|
||||
<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} />
|
||||
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
||||
<NotAuthorizedView
|
||||
@ -117,7 +113,15 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
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 && (
|
||||
<AppHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
@ -126,34 +130,11 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex w-full flex-grow flex-col ${
|
||||
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>
|
||||
)}
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,6 +1,10 @@
|
||||
require("dotenv").config({ path: ".env" });
|
||||
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const path = require("path");
|
||||
const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "").split(",").filter((domain) => domain.length > 0);
|
||||
const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "")
|
||||
.split(",")
|
||||
.filter((domain) => domain.length > 0);
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
|
@ -23,6 +23,7 @@
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"axios": "^1.1.3",
|
||||
"cmdk": "^0.2.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "12.3.2",
|
||||
|
@ -45,7 +45,7 @@ const WorkspacePage: NextPage = () => {
|
||||
isOpen={isProductUpdatesModalOpen}
|
||||
setIsOpen={setIsProductUpdatesModalOpen}
|
||||
/>
|
||||
<div className="h-full w-full">
|
||||
<div className="p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
<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"
|
||||
|
@ -43,7 +43,6 @@ const MyIssuesPage: NextPage = () => {
|
||||
<BreadcrumbItem title="My Issues" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
noPadding
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
{myIssues && myIssues.length > 0 && (
|
||||
@ -52,7 +51,7 @@ const MyIssuesPage: NextPage = () => {
|
||||
<>
|
||||
<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 ${
|
||||
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>
|
||||
@ -69,9 +68,8 @@ const MyIssuesPage: NextPage = () => {
|
||||
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">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-brand-secondary">Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate") return null;
|
||||
@ -80,10 +78,10 @@ const MyIssuesPage: NextPage = () => {
|
||||
<button
|
||||
key={key}
|
||||
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]
|
||||
? "border-theme bg-theme text-white"
|
||||
: ""
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
@ -93,7 +91,6 @@ const MyIssuesPage: NextPage = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
@ -107,7 +104,7 @@ const MyIssuesPage: NextPage = () => {
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
@ -117,24 +114,12 @@ const MyIssuesPage: NextPage = () => {
|
||||
{myIssues ? (
|
||||
<>
|
||||
{myIssues.length > 0 ? (
|
||||
<div className="flex flex-col space-y-5">
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="rounded-[10px] border border-brand-base bg-brand-base">
|
||||
<div
|
||||
className={`flex items-center justify-start bg-brand-surface-1 px-4 py-2.5 ${
|
||||
open ? "rounded-t-[10px]" : "rounded-[10px]"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center px-4 py-2.5">
|
||||
<Disclosure.Button>
|
||||
<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>
|
||||
<span className="rounded-full bg-brand-surface-2 py-0.5 px-3 text-sm text-brand-secondary">
|
||||
{myIssues.length}
|
||||
@ -165,7 +150,6 @@ const MyIssuesPage: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center px-4">
|
||||
<EmptySpace
|
||||
|
@ -11,6 +11,7 @@ import { Loader } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// fetch-keys
|
||||
import { USER_ACTIVITY } from "constants/fetch-keys";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
|
||||
const ProfileActivity = () => {
|
||||
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
|
||||
@ -25,8 +26,17 @@ const ProfileActivity = () => {
|
||||
<BreadcrumbItem title="My Profile Activity" />
|
||||
</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.results.length > 0 ? (
|
||||
<Feeds activities={userActivity.results} />
|
||||
@ -39,6 +49,7 @@ const ProfileActivity = () => {
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ import type { NextPage } from "next";
|
||||
import type { IUser } from "types";
|
||||
// constants
|
||||
import { USER_ROLES } from "constants/workspace";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
|
||||
const defaultValues: Partial<IUser> = {
|
||||
avatar: "",
|
||||
@ -130,7 +131,6 @@ const Profile: NextPage = () => {
|
||||
<BreadcrumbItem title="My Profile" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
profilePage
|
||||
>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
@ -144,6 +144,16 @@ const Profile: NextPage = () => {
|
||||
userImage
|
||||
/>
|
||||
{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="grid grid-cols-12 gap-4 sm:gap-16">
|
||||
<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="col-span-12 sm:col-span-6">
|
||||
<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 className="col-span-12 sm:col-span-6">
|
||||
<Input
|
||||
@ -285,6 +297,7 @@ const Profile: NextPage = () => {
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||
<Spinner />
|
||||
|
@ -124,7 +124,7 @@ const ProjectCycles: NextPage = () => {
|
||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||
data={selectedCycle}
|
||||
/>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-8 p-8">
|
||||
<div className="flex flex-col gap-5">
|
||||
{currentAndUpcomingCycles && currentAndUpcomingCycles.current_cycle.length > 0 && (
|
||||
<h3 className="text-3xl font-semibold text-brand-base">Current Cycle</h3>
|
||||
|
@ -122,7 +122,6 @@ const IssueDetailsPage: NextPage = () => {
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
noPadding
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
|
@ -77,7 +77,7 @@ const ProjectModules: NextPage = () => {
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Module
|
||||
</PrimaryButton>
|
||||
}
|
||||
@ -89,7 +89,7 @@ const ProjectModules: NextPage = () => {
|
||||
/>
|
||||
{modules ? (
|
||||
modules.length > 0 ? (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 p-8">
|
||||
<div className="flex flex-col gap-5">
|
||||
<h3 className="text-3xl font-semibold text-brand-base">Modules</h3>
|
||||
|
||||
|
@ -312,7 +312,7 @@ const SinglePage: NextPage = () => {
|
||||
}
|
||||
>
|
||||
{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">
|
||||
<button
|
||||
type="button"
|
||||
@ -543,7 +543,6 @@ const SinglePage: NextPage = () => {
|
||||
<CreateUpdateBlockInline
|
||||
handleClose={() => setCreateBlockForm(false)}
|
||||
focus="name"
|
||||
setGptAssistantModal={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -195,7 +195,7 @@ const ProjectPages: NextPage = () => {
|
||||
</PrimaryButton>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 p-8">
|
||||
<form
|
||||
onSubmit={handleSubmit(createPage)}
|
||||
className="relative mb-12 flex items-center justify-between gap-2 rounded-[6px] border border-brand-base p-2 shadow"
|
||||
|
@ -20,6 +20,7 @@ import { IProject, IWorkspace } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const defaultValues: Partial<IProject> = {
|
||||
project_lead: null,
|
||||
@ -103,7 +104,8 @@ const ControlSettings: NextPage = () => {
|
||||
</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="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
|
@ -27,6 +27,7 @@ import { IEstimate, IProject } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const EstimatesSettings: NextPage = () => {
|
||||
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
|
||||
@ -98,6 +99,14 @@ const EstimatesSettings: NextPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateEstimateModal
|
||||
isOpen={estimateFormOpen}
|
||||
data={estimateToUpdate}
|
||||
handleClose={() => {
|
||||
setEstimateFormOpen(false);
|
||||
setEstimateToUpdate(undefined);
|
||||
}}
|
||||
/>
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
@ -109,14 +118,8 @@ const EstimatesSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<CreateUpdateEstimateModal
|
||||
isOpen={estimateFormOpen}
|
||||
data={estimateToUpdate}
|
||||
handleClose={() => {
|
||||
setEstimateFormOpen(false);
|
||||
setEstimateToUpdate(undefined);
|
||||
}}
|
||||
/>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">Estimates</h3>
|
||||
<div className="col-span-12 space-y-5 sm:col-span-7">
|
||||
@ -139,7 +142,7 @@ const EstimatesSettings: NextPage = () => {
|
||||
</section>
|
||||
{estimatesList ? (
|
||||
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) => (
|
||||
<SingleEstimate
|
||||
key={estimate.id}
|
||||
@ -171,6 +174,7 @@ const EstimatesSettings: NextPage = () => {
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -22,6 +22,7 @@ import { IProject } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const featuresList = [
|
||||
{
|
||||
@ -134,6 +135,8 @@ const FeaturesSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-2xl font-semibold">Features</h3>
|
||||
<div className="space-y-5">
|
||||
@ -182,6 +185,7 @@ const FeaturesSettings: NextPage = () => {
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// components
|
||||
import { DeleteProjectModal } from "components/project";
|
||||
import { DeleteProjectModal, SettingsHeader } from "components/project";
|
||||
import { ImagePickerPopover } from "components/core";
|
||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||
// hooks
|
||||
@ -151,7 +151,8 @@ const GeneralSettings: NextPage = () => {
|
||||
router.push(`/${workspaceSlug}/projects`);
|
||||
}}
|
||||
/>
|
||||
<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="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
||||
<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="col-span-12 sm:col-span-6">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -10,12 +10,13 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
import IntegrationService from "services/integration";
|
||||
import projectService from "services/project.service";
|
||||
// components
|
||||
import { SingleIntegration } from "components/project";
|
||||
import { SettingsHeader, SingleIntegration } from "components/project";
|
||||
// ui
|
||||
import { EmptySpace, EmptySpaceItem, Loader } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
import type { NextPage } from "next";
|
||||
@ -53,10 +54,26 @@ const ProjectIntegrations: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
{workspaceIntegrations ? (
|
||||
workspaceIntegrations.length > 0 ? (
|
||||
<section className="space-y-8">
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<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">
|
||||
{workspaceIntegrations.map((integration) => (
|
||||
<SingleIntegration
|
||||
@ -91,6 +108,7 @@ const ProjectIntegrations: NextPage = () => {
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -7,8 +7,6 @@ import useSWR from "swr";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// lib
|
||||
import { requiredAdmin } from "lib/auth";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
@ -24,10 +22,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssueLabels, UserAuth } from "types";
|
||||
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||
import { IIssueLabels } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const LabelsSettings: NextPage = () => {
|
||||
// create/edit label form
|
||||
@ -103,6 +102,8 @@ const LabelsSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="grid grid-cols-12 gap-10">
|
||||
<div className="col-span-12 sm:col-span-5">
|
||||
<h3 className="text-2xl font-semibold">Labels</h3>
|
||||
@ -173,6 +174,7 @@ const LabelsSettings: NextPage = () => {
|
||||
</>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -27,6 +27,7 @@ import type { NextPage } from "next";
|
||||
import { PROJECT_INVITATIONS, PROJECT_MEMBERS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { ROLE } from "constants/workspace";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const MembersSettings: NextPage = () => {
|
||||
const [inviteModal, setInviteModal] = useState(false);
|
||||
@ -141,6 +142,8 @@ const MembersSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">Members</h3>
|
||||
@ -255,6 +258,7 @@ const MembersSettings: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -28,6 +28,7 @@ import { getStatesList, orderStateGroups } from "helpers/state.helper";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
import { SettingsHeader } from "components/project";
|
||||
|
||||
const StatesSettings: NextPage = () => {
|
||||
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
|
||||
@ -66,6 +67,8 @@ const StatesSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<div className="grid grid-cols-12 gap-10">
|
||||
<div className="col-span-12 sm:col-span-5">
|
||||
<h3 className="text-2xl font-semibold text-brand-base">States</h3>
|
||||
@ -141,6 +144,7 @@ const StatesSettings: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -97,7 +97,7 @@ const ProjectViews: NextPage = () => {
|
||||
/>
|
||||
{views ? (
|
||||
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>
|
||||
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
|
||||
{views.map((view) => (
|
||||
|
@ -83,7 +83,7 @@ const ProjectsPage: NextPage = () => {
|
||||
data={projects?.find((item) => item.id === deleteProject) ?? null}
|
||||
/>
|
||||
{projects ? (
|
||||
<>
|
||||
<div className="p-8">
|
||||
{projects.length === 0 ? (
|
||||
<EmptyState
|
||||
type="project"
|
||||
@ -103,7 +103,7 @@ const ProjectsPage: NextPage = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="grid grid-cols-3 gap-4">
|
||||
<Loader.Item height="100px" />
|
||||
|
@ -8,6 +8,7 @@ import useSWR from "swr";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { SettingsHeader } from "components/workspace";
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
@ -34,10 +35,12 @@ const BillingSettings: NextPage = () => {
|
||||
title={`${activeWorkspace?.name ?? "Workspace"}`}
|
||||
link={`/${workspaceSlug}`}
|
||||
/>
|
||||
<BreadcrumbItem title="Members Settings" />
|
||||
<BreadcrumbItem title="Billing & Plans Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold leading-6">Billing & Plans</h3>
|
||||
@ -65,6 +68,7 @@ const BillingSettings: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { SettingsHeader } from "components/workspace";
|
||||
// components
|
||||
import IntegrationGuide from "components/integration/guide";
|
||||
// ui
|
||||
@ -18,11 +19,14 @@ const ImportExport: NextPage = () => {
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title={`${workspaceSlug ?? "Workspace"}`} link={`/${workspaceSlug}`} />
|
||||
<BreadcrumbItem title="Members Settings" />
|
||||
<BreadcrumbItem title="Import/ Export Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<IntegrationGuide />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -14,9 +14,10 @@ import fileService from "services/file.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
// components
|
||||
import { ImageUploadModal } from "components/core";
|
||||
import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
|
||||
// ui
|
||||
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
@ -172,6 +173,8 @@ const WorkspaceSettings: NextPage = () => {
|
||||
}}
|
||||
data={activeWorkspace ?? null}
|
||||
/>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
{activeWorkspace ? (
|
||||
<div className="space-y-8 sm:space-y-12">
|
||||
<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>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
The danger zone of the workspace delete page is a critical area that requires
|
||||
careful consideration and attention. When deleting a workspace, all of the data and
|
||||
resources within that workspace will be permanently removed and cannot be recovered.
|
||||
careful consideration and attention. When deleting a workspace, all of the data
|
||||
and resources within that workspace will be permanently removed and cannot be
|
||||
recovered.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
@ -329,6 +333,7 @@ const WorkspaceSettings: NextPage = () => {
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -9,11 +9,14 @@ import workspaceService from "services/workspace.service";
|
||||
import IntegrationService from "services/integration";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { SettingsHeader } from "components/workspace";
|
||||
// components
|
||||
import { SingleIntegrationCard } from "components/integration";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { ExclamationIcon } from "components/icons";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
@ -44,8 +47,20 @@ const WorkspaceIntegrations: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-8">
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<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">
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
@ -59,6 +74,7 @@ const WorkspaceIntegrations: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import useToast from "hooks/use-toast";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { SettingsHeader } from "components/workspace";
|
||||
// components
|
||||
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
|
||||
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
|
||||
@ -137,6 +138,8 @@ const MembersSettings: NextPage = () => {
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="px-24 py-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">Members</h3>
|
||||
@ -248,6 +251,7 @@ const MembersSettings: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import "styles/globals.css";
|
||||
import "styles/editor.css";
|
||||
import "styles/command-pallette.css";
|
||||
import "styles/nprogress.css";
|
||||
import "styles/react-datepicker.css";
|
||||
|
||||
// router
|
||||
import Router from "next/router";
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Image from "next/image";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useToast from "hooks/use-toast";
|
||||
|
@ -1,6 +1,21 @@
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const unAuthorizedStatus = [401];
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const { status }: any = error.response;
|
||||
if (unAuthorizedStatus.includes(status)) {
|
||||
Cookies.remove("refreshToken", { path: "/" });
|
||||
Cookies.remove("accessToken", { path: "/" });
|
||||
console.log("window.location.href", window.location.pathname);
|
||||
if (window.location.pathname != "/signin") window.location.href = "/signin";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
abstract class APIService {
|
||||
protected baseURL: string;
|
||||
protected headers: any = {};
|
||||
|
@ -1,10 +1,14 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
// types
|
||||
import type { IEstimate, IEstimateFormData, IEstimatePoint } from "types";
|
||||
import type { IEstimate, IEstimateFormData } from "types";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ProjectEstimateServices extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -16,7 +20,11 @@ class ProjectEstimateServices extends APIService {
|
||||
data: IEstimateFormData
|
||||
): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE");
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
@ -32,7 +40,11 @@ class ProjectEstimateServices extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE");
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
@ -64,7 +76,11 @@ class ProjectEstimateServices extends APIService {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE");
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,10 +1,14 @@
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
import { IGithubRepoInfo, IGithubServiceImportFormData } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const integrationServiceType: string = "github";
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
const integrationServiceType: string = "github";
|
||||
class GithubIntegrationService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -41,7 +45,11 @@ class GithubIntegrationService extends APIService {
|
||||
`/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE");
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,9 +1,14 @@
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import { IAppIntegration, IImporterService, IWorkspaceIntegration } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class IntegrationService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -49,7 +54,12 @@ class IntegrationService extends APIService {
|
||||
importerId: string
|
||||
): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
||||
.then((res) => res?.data)
|
||||
.then((response) => {
|
||||
const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE";
|
||||
|
||||
if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -1,10 +1,14 @@
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class JiraImportedService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
@ -22,7 +26,11 @@ class JiraImportedService extends APIService {
|
||||
|
||||
async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise<IJiraResponse> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
|
||||
.then((response) => response?.data)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE");
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ const trackEvent =
|
||||
// types
|
||||
import type {
|
||||
ICycle,
|
||||
IEstimate,
|
||||
IGptResponse,
|
||||
IIssue,
|
||||
IIssueComment,
|
||||
@ -45,7 +46,10 @@ type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
|
||||
|
||||
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
|
||||
|
||||
type IssueCommentType = "ISSUE_COMMENT_CREATE" | "ISSUE_COMMENT_UPDATE" | "ISSUE_COMMENT_DELETE";
|
||||
type IssueCommentEventType =
|
||||
| "ISSUE_COMMENT_CREATE"
|
||||
| "ISSUE_COMMENT_UPDATE"
|
||||
| "ISSUE_COMMENT_DELETE";
|
||||
|
||||
export type MiscellaneousEventType =
|
||||
| "TOGGLE_CYCLE_ON"
|
||||
@ -73,6 +77,13 @@ type IssueLabelEventType = "ISSUE_LABEL_CREATE" | "ISSUE_LABEL_UPDATE" | "ISSUE_
|
||||
|
||||
type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_IN_PAGE_BLOCK";
|
||||
|
||||
type IssueEstimateEventType = "ESTIMATE_CREATE" | "ESTIMATE_UPDATE" | "ESTIMATE_DELETE";
|
||||
|
||||
type ImporterEventType =
|
||||
| "GITHUB_IMPORTER_CREATE"
|
||||
| "GITHUB_IMPORTER_DELETE"
|
||||
| "JIRA_IMPORTER_CREATE"
|
||||
| "JIRA_IMPORTER_DELETE";
|
||||
class TrackEventServices extends APIService {
|
||||
constructor() {
|
||||
super("/");
|
||||
@ -209,7 +220,7 @@ class TrackEventServices extends APIService {
|
||||
|
||||
async trackIssueCommentEvent(
|
||||
data: Partial<IIssueComment> | any,
|
||||
eventName: IssueCommentType
|
||||
eventName: IssueCommentEventType
|
||||
): Promise<any> {
|
||||
let payload: any;
|
||||
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();
|
||||
|
@ -112,6 +112,7 @@ body {
|
||||
.horizontal-scroll-enable::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 7px;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.horizontal-scroll-enable::-webkit-scrollbar-track {
|
||||
|
118
apps/app/styles/react-datepicker.css
Normal file
118
apps/app/styles/react-datepicker.css
Normal 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;
|
||||
}
|
@ -12,10 +12,6 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
theme: "#3f76ff",
|
||||
"hover-gray": "#f5f5f5",
|
||||
primary: "#f9fafb", // gray-50
|
||||
secondary: "white",
|
||||
brand: {
|
||||
accent: withOpacity("--color-accent"),
|
||||
base: withOpacity("--color-bg-base"),
|
||||
|
2
apps/app/types/estimate.d.ts
vendored
2
apps/app/types/estimate.d.ts
vendored
@ -8,7 +8,9 @@ export interface IEstimate {
|
||||
updated_by: string;
|
||||
points: IEstimatePoint[];
|
||||
project: string;
|
||||
project_detail: IProject;
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspace;
|
||||
}
|
||||
|
||||
export interface IEstimatePoint {
|
||||
|
@ -35,24 +35,46 @@ services:
|
||||
- redisdata:/data
|
||||
plane-web:
|
||||
container_name: planefrontend
|
||||
image: makeplane/plane-frontend:0.5-dev
|
||||
image: makeplane/plane-frontend:0.6
|
||||
restart: always
|
||||
command: node apps/app/server.js
|
||||
env_file:
|
||||
- ./apps/app/.env
|
||||
command: [ "/usr/local/bin/start.sh" ]
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID: 0
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME: 0
|
||||
NEXT_PUBLIC_GITHUB_ID: 0
|
||||
NEXT_PUBLIC_SENTRY_DSN: 0
|
||||
NEXT_PUBLIC_ENABLE_OAUTH: 0
|
||||
NEXT_PUBLIC_ENABLE_SENTRY: 0
|
||||
ports:
|
||||
- 3000:3000
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
image: makeplane/plane-backend:0.5-dev
|
||||
image: makeplane/plane-backend:0.6
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane
|
||||
REDIS_URL: redis://redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
||||
WEB_URL: localhost/
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
||||
DISABLE_COLLECTSTATIC: 1
|
||||
DOCKERIZED: 1
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
GPT_ENGINE: ${GPT_ENGINE}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
@ -62,7 +84,7 @@ services:
|
||||
- redis:redis
|
||||
plane-worker:
|
||||
container_name: planerqworker
|
||||
image: makeplane/plane-worker:0.5-dev
|
||||
image: makeplane/plane-worker:0.6
|
||||
depends_on:
|
||||
- redis
|
||||
- db
|
||||
@ -71,8 +93,24 @@ services:
|
||||
links:
|
||||
- redis:redis
|
||||
- db:db
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane
|
||||
REDIS_URL: redis://redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
||||
WEB_URL: localhost/
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
||||
DISABLE_COLLECTSTATIC: 1
|
||||
DOCKERIZED: 1
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
GPT_ENGINE: ${GPT_ENGINE}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
|
@ -38,12 +38,21 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/app/Dockerfile.web
|
||||
restart: always
|
||||
command: node apps/app/server.js
|
||||
env_file:
|
||||
- ./apps/app/.env
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
||||
command: [ "/usr/local/bin/start.sh" ]
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
||||
NEXT_PUBLIC_SENTRY_DSN: "0"
|
||||
NEXT_PUBLIC_ENABLE_OAUTH: "0"
|
||||
NEXT_PUBLIC_ENABLE_SENTRY: "0"
|
||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
|
||||
NEXT_PUBLIC_TRACK_EVENTS: "0"
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
build:
|
||||
@ -52,8 +61,24 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane
|
||||
REDIS_URL: redis://redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
||||
WEB_URL: localhost/
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
||||
DISABLE_COLLECTSTATIC: 1
|
||||
DOCKERIZED: 1
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
GPT_ENGINE: ${GPT_ENGINE}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
@ -74,8 +99,24 @@ services:
|
||||
links:
|
||||
- redis:redis
|
||||
- db:db
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane
|
||||
REDIS_URL: redis://redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
||||
WEB_URL: localhost/
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
||||
DISABLE_COLLECTSTATIC: 1
|
||||
DOCKERIZED: 1
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
GPT_ENGINE: ${GPT_ENGINE}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
|
@ -2,7 +2,7 @@
|
||||
nodaemon=true
|
||||
|
||||
[program:node]
|
||||
command=node /app/apps/app/server.js
|
||||
command=sh /usr/local/bin/start.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/node.err.log
|
||||
@ -22,3 +22,11 @@ autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/nginx.err.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
17
replace-env-vars.sh
Normal 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
|
8
setup.sh
8
setup.sh
@ -1,9 +1,7 @@
|
||||
#!/bin/bash
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
# Generating App environmental variables
|
||||
cp ./apps/app/.env.example ./apps/app/.env
|
||||
cp ./.env.example ./.env
|
||||
|
||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./apps/app/.env
|
||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env
|
||||
export LC_ALL=C
|
||||
export LC_CTYPE=C
|
||||
echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env
|
||||
|
9
start.sh
Normal file
9
start.sh
Normal 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
|
@ -4,6 +4,7 @@
|
||||
"NEXT_PUBLIC_GITHUB_ID",
|
||||
"NEXT_PUBLIC_GOOGLE_CLIENTID",
|
||||
"NEXT_PUBLIC_API_BASE_URL",
|
||||
"API_BASE_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
@ -17,6 +18,7 @@
|
||||
"NEXT_PUBLIC_CRISP_ID",
|
||||
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
||||
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
||||
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
||||
"NEXT_PUBLIC_SLACK_CLIENT_ID",
|
||||
"NEXT_PUBLIC_SLACK_CLIENT_SECRET"
|
||||
],
|
||||
|
@ -4229,6 +4229,11 @@ dot-case@^3.0.4:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
dotenv@^16.0.3:
|
||||
version "16.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
|
||||
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
|
||||
|
||||
ejs@^3.1.6:
|
||||
version "3.1.8"
|
||||
resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user