Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0397ef2b5 | |||
| a2825208b8 | |||
| c3387ba974 | |||
| baa9c30449 | |||
| 849e2d658a | |||
| c7f1090914 | |||
| e3d43298df | |||
| b36565298f | |||
| b26e8bd956 | |||
| e5703dbe70 | |||
| 498d6d2b02 | |||
| 1d22817ede | |||
| 483c49d0ff | |||
| 0fa9451633 | |||
| 46237c5431 | |||
| 20e400487f | |||
| 99fb3c9bfe | |||
| 88200a93bf | |||
| b2ad071608 | |||
| 21992f540f | |||
| 742731cbe6 | |||
| c6878b9b0f | |||
| 887cac5612 | |||
| 982566b5b4 | |||
| 2dc5655886 | |||
| 160b4a4390 | |||
| f187d9512a | |||
| 53d3ea1979 | |||
| 13d76d4325 | |||
| 25338cc804 | |||
| eaa750b025 | |||
| 81d9c70026 | |||
| 93fb4fe1e9 | |||
| b8e6d072cc | |||
| 1220cebe50 | |||
| 9464b5c00e | |||
| 3fae0f39c0 | |||
| 73a757e337 | |||
| bb40b7feb5 | |||
| 3175ce9136 | |||
| 0b9b4bb289 | |||
| f0f24b6fc4 | |||
| 429dffb055 | |||
| 6e5c85cd6e | |||
| 2adcb163fb | |||
| 028a350cd1 | |||
| d021a5696a | |||
| a5c18e37c1 | |||
| 8e611664a8 | |||
| 3480b450f2 | |||
| a7d9591c44 | |||
| 1364c842e0 | |||
| eb99b4adc9 | |||
| 6684dd4ab6 | |||
| 529ed4432c | |||
| 8c1ad69f0c | |||
| c23de32b03 | |||
| 83ac1f4e4c | |||
| cee9695a4a | |||
| c9f866e538 | |||
| 7d96adcb70 | |||
| c5b034385f | |||
| ff7f31c35b | |||
| 7234d6f68b | |||
| d8a5b8d848 | |||
| 5412e09701 | |||
| 7116acc331 | |||
| ae26b17cab | |||
| 2ec8fbab34 | |||
| 213dc3f8e8 | |||
| 4b02886c40 | |||
| 8eddc4b304 | |||
| 169a60723b | |||
| c80094581e | |||
| d99f669b89 | |||
| 99dd1b9f0c | |||
| 48e77ea81b | |||
| 0be6738715 | |||
| d041d8be6b | |||
| e53847c59e | |||
| 16781a71fe | |||
| fb4535b294 | |||
| d2a58bf04a | |||
| c51407c85e | |||
| 3817511024 | |||
| 2950877767 | |||
| 3d6f2dd3dc |
@@ -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=""
|
||||
@@ -9,3 +8,13 @@ NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
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=""
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `eslint-config-custom`
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["app/"],
|
||||
rootDir: ["apps/*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
+19
-1
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -3,7 +3,15 @@ import uuid
|
||||
import random
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from plane.db.models import ProjectIdentifier
|
||||
from plane.db.models import Issue, IssueComment, User, Project, ProjectMember, Label
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueComment,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
Label,
|
||||
Integration,
|
||||
)
|
||||
|
||||
|
||||
# Update description and description html values for old descriptions
|
||||
@@ -174,3 +182,29 @@ def update_label_color():
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def create_slack_integration():
|
||||
try:
|
||||
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
|
||||
def update_integration_verified():
|
||||
try:
|
||||
integrations = Integration.objects.all()
|
||||
updated_integrations = []
|
||||
for integration in integrations:
|
||||
integration.verified = True
|
||||
updated_integrations.append(integration)
|
||||
|
||||
Integration.objects.bulk_update(
|
||||
updated_integrations, ["verified"], batch_size=10
|
||||
)
|
||||
print("Sucess")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Failed")
|
||||
|
||||
@@ -62,6 +62,7 @@ from .integration import (
|
||||
GithubRepositorySerializer,
|
||||
GithubRepositorySyncSerializer,
|
||||
GithubCommentSyncSerializer,
|
||||
SlackProjectSyncSerializer,
|
||||
)
|
||||
|
||||
from .importer import ImporterSerializer
|
||||
|
||||
@@ -5,3 +5,4 @@ from .github import (
|
||||
GithubIssueSyncSerializer,
|
||||
GithubCommentSyncSerializer,
|
||||
)
|
||||
from .slack import SlackProjectSyncSerializer
|
||||
@@ -0,0 +1,14 @@
|
||||
# Module imports
|
||||
from plane.api.serializers import BaseSerializer
|
||||
from plane.db.models import SlackProjectSync
|
||||
|
||||
|
||||
class SlackProjectSyncSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = SlackProjectSync
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"project",
|
||||
"workspace",
|
||||
"workspace_integration",
|
||||
]
|
||||
@@ -79,7 +79,6 @@ from plane.api.views import (
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
StateDeleteIssueCheckEndpoint,
|
||||
## End States
|
||||
# Estimates
|
||||
ProjectEstimatePointEndpoint,
|
||||
@@ -132,6 +131,7 @@ from plane.api.views import (
|
||||
GithubIssueSyncViewSet,
|
||||
GithubCommentSyncViewSet,
|
||||
BulkCreateGithubIssueSyncEndpoint,
|
||||
SlackProjectSyncViewSet,
|
||||
## End Integrations
|
||||
# Importer
|
||||
ServiceIssueImportSummaryEndpoint,
|
||||
@@ -508,11 +508,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project-state",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
|
||||
StateDeleteIssueCheckEndpoint.as_view(),
|
||||
name="state-delete-check",
|
||||
),
|
||||
# End States ##
|
||||
# Estimates
|
||||
path(
|
||||
@@ -1216,6 +1211,26 @@ urlpatterns = [
|
||||
),
|
||||
),
|
||||
## End Github Integrations
|
||||
# Slack Integration
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/",
|
||||
SlackProjectSyncViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/project-slack-sync/<uuid:pk>/",
|
||||
SlackProjectSyncViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
"get": "retrieve",
|
||||
}
|
||||
),
|
||||
),
|
||||
## End Slack Integration
|
||||
## End Integrations
|
||||
# Importer
|
||||
path(
|
||||
|
||||
@@ -42,7 +42,7 @@ from .workspace import (
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
WorkspaceThemeViewSet,
|
||||
)
|
||||
from .state import StateViewSet, StateDeleteIssueCheckEndpoint
|
||||
from .state import StateViewSet
|
||||
from .shortcut import ShortCutViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .cycle import (
|
||||
@@ -106,6 +106,7 @@ from .integration import (
|
||||
GithubCommentSyncViewSet,
|
||||
GithubRepositoriesEndpoint,
|
||||
BulkCreateGithubIssueSyncEndpoint,
|
||||
SlackProjectSyncViewSet,
|
||||
)
|
||||
|
||||
from .importer import (
|
||||
|
||||
@@ -48,6 +48,28 @@ class CycleViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"cycle_id": str(self.kwargs.get("pk")),
|
||||
"issues": [str(issue_id) for issue_id in cycle_issues],
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = CycleFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
@@ -181,6 +203,22 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
cycle_id=self.kwargs.get("cycle_id"),
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"cycle_id": str(self.kwargs.get("cycle_id")),
|
||||
"issues": [str(instance.issue_id)],
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
@@ -286,9 +324,9 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
# Get all CycleIssues already created
|
||||
cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues))
|
||||
records_to_update = []
|
||||
update_cycle_issue_activity = []
|
||||
record_to_create = []
|
||||
records_to_update = []
|
||||
|
||||
for issue in issues:
|
||||
cycle_issue = [
|
||||
@@ -333,7 +371,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": issues}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
|
||||
@@ -28,6 +28,7 @@ from plane.db.models import (
|
||||
Module,
|
||||
ModuleLink,
|
||||
ModuleIssue,
|
||||
Label,
|
||||
)
|
||||
from plane.api.serializers import (
|
||||
ImporterSerializer,
|
||||
@@ -235,9 +236,20 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
|
||||
def delete(self, request, slug, service, pk):
|
||||
try:
|
||||
importer = Importer.objects.filter(
|
||||
importer = Importer.objects.get(
|
||||
pk=pk, service=service, workspace__slug=slug
|
||||
)
|
||||
# Delete all imported Issues
|
||||
imported_issues = importer.imported_data.get("issues", [])
|
||||
Issue.objects.filter(id__in=imported_issues).delete()
|
||||
|
||||
# Delete all imported Labels
|
||||
imported_labels = importer.imported_data.get("labels", [])
|
||||
Label.objects.filter(id__in=imported_labels).delete()
|
||||
|
||||
if importer.service == "jira":
|
||||
imported_modules = importer.imported_data.get("modules", [])
|
||||
Module.objects.filter(id__in=imported_modules).delete()
|
||||
importer.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Exception as e:
|
||||
@@ -247,6 +259,27 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, service, pk):
|
||||
try:
|
||||
importer = Importer.objects.get(
|
||||
pk=pk, service=service, workspace__slug=slug
|
||||
)
|
||||
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Importer.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class UpdateServiceImportStatusEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id, service, importer_id):
|
||||
@@ -487,48 +520,59 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
_ = ModuleLink.objects.bulk_create(
|
||||
[
|
||||
ModuleLink(
|
||||
module=module,
|
||||
url=module_data.get("link", {}).get("url", "https://plane.so"),
|
||||
title=module_data.get("link", {}).get(
|
||||
"title", "Original Issue"
|
||||
),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for module, module_data in zip(modules, modules_data)
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
||||
|
||||
bulk_module_issues = []
|
||||
for module, module_data in zip(modules, modules_data):
|
||||
module_issues_list = module_data.get("module_issues_list", [])
|
||||
bulk_module_issues = bulk_module_issues + [
|
||||
ModuleIssue(
|
||||
issue_id=issue,
|
||||
module=module,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for issue in module_issues_list
|
||||
]
|
||||
if len(modules) == len(modules_data):
|
||||
_ = ModuleLink.objects.bulk_create(
|
||||
[
|
||||
ModuleLink(
|
||||
module=module,
|
||||
url=module_data.get("link", {}).get(
|
||||
"url", "https://plane.so"
|
||||
),
|
||||
title=module_data.get("link", {}).get(
|
||||
"title", "Original Issue"
|
||||
),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for module, module_data in zip(modules, modules_data)
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
bulk_module_issues, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
bulk_module_issues = []
|
||||
for module, module_data in zip(modules, modules_data):
|
||||
module_issues_list = module_data.get("module_issues_list", [])
|
||||
bulk_module_issues = bulk_module_issues + [
|
||||
ModuleIssue(
|
||||
issue_id=issue,
|
||||
module=module,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for issue in module_issues_list
|
||||
]
|
||||
|
||||
serializer = ModuleSerializer(modules, many=True)
|
||||
return Response(
|
||||
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
bulk_module_issues, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
|
||||
serializer = ModuleSerializer(modules, many=True)
|
||||
return Response(
|
||||
{"modules": serializer.data}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
else:
|
||||
return Response(
|
||||
{"message": "Modules created but issues could not be imported"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Project.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -6,3 +6,4 @@ from .github import (
|
||||
GithubCommentSyncViewSet,
|
||||
GithubRepositoriesEndpoint,
|
||||
)
|
||||
from .slack import SlackProjectSyncViewSet
|
||||
|
||||
@@ -27,6 +27,7 @@ from plane.utils.integrations.github import (
|
||||
)
|
||||
from plane.api.permissions import WorkSpaceAdminPermission
|
||||
|
||||
|
||||
class IntegrationViewSet(BaseViewSet):
|
||||
serializer_class = IntegrationSerializer
|
||||
model = Integration
|
||||
@@ -101,7 +102,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
@@ -112,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
|
||||
def create(self, request, slug, provider):
|
||||
try:
|
||||
installation_id = request.data.get("installation_id", None)
|
||||
|
||||
if not installation_id:
|
||||
return Response(
|
||||
{"error": "Installation ID is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
integration = Integration.objects.get(provider=provider)
|
||||
config = {}
|
||||
if provider == "github":
|
||||
installation_id = request.data.get("installation_id", None)
|
||||
if not installation_id:
|
||||
return Response(
|
||||
{"error": "Installation ID is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
metadata = get_github_metadata(installation_id)
|
||||
config = {"installation_id": installation_id}
|
||||
|
||||
if provider == "slack":
|
||||
metadata = request.data.get("metadata", {})
|
||||
access_token = metadata.get("access_token", False)
|
||||
team_id = metadata.get("team", {}).get("id", False)
|
||||
if not metadata or not access_token or not team_id:
|
||||
return Response(
|
||||
{"error": "Access token and team id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
config = {"team_id": team_id, "access_token": access_token}
|
||||
|
||||
# Create a bot user
|
||||
bot_user = User.objects.create(
|
||||
email=f"{uuid.uuid4().hex}@plane.so",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Django import
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.api.views import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
|
||||
from plane.api.serializers import SlackProjectSyncSerializer
|
||||
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
|
||||
|
||||
|
||||
class SlackProjectSyncViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
]
|
||||
serializer_class = SlackProjectSyncSerializer
|
||||
model = SlackProjectSync
|
||||
|
||||
def create(self, request, slug, project_id, workspace_integration_id):
|
||||
try:
|
||||
serializer = SlackProjectSyncSerializer(data=request.data)
|
||||
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
workspace__slug=slug, pk=workspace_integration_id
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
project_id=project_id,
|
||||
workspace_integration_id=workspace_integration_id,
|
||||
)
|
||||
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
pk=workspace_integration_id, workspace__slug=slug
|
||||
)
|
||||
|
||||
_ = ProjectMember.objects.get_or_create(
|
||||
member=workspace_integration.actor, role=20, project_id=project_id
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except WorkspaceIntegration.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace Integration does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -109,6 +109,28 @@ class ModuleViewSet(BaseViewSet):
|
||||
.order_by("-is_favorite", "name")
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(self.kwargs.get("pk")),
|
||||
"issues": [str(issue_id) for issue_id in module_issues],
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
@@ -158,6 +180,22 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
module_id=self.kwargs.get("module_id"),
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(self.kwargs.get("module_id")),
|
||||
"issues": [str(instance.issue_id)],
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
@@ -302,7 +340,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"modules_list": issues}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
|
||||
@@ -96,6 +96,36 @@ class PageViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
# Only update access if the page owner is the requesting user
|
||||
if (
|
||||
page.access != request.data.get("access", page.access)
|
||||
and page.owned_by_id != request.user.id
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
serializer = PageSerializer(page, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Page.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class PageBlockViewSet(BaseViewSet):
|
||||
serializer_class = PageBlockSerializer
|
||||
|
||||
@@ -103,22 +103,3 @@ class StateViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except State.DoesNotExist:
|
||||
return Response({"error": "State does not exists"}, status=status.HTTP_404)
|
||||
|
||||
|
||||
class StateDeleteIssueCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, pk):
|
||||
try:
|
||||
issue_count = Issue.objects.filter(
|
||||
state=pk, workspace__slug=slug, project_id=project_id
|
||||
).count()
|
||||
return Response({"issue_count": issue_count}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ from plane.db.models import (
|
||||
User,
|
||||
)
|
||||
from .workspace_invitation_task import workspace_invitation
|
||||
from plane.bgtasks.user_welcome_task import send_welcome_email
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -40,7 +41,7 @@ def service_importer(service, importer_id):
|
||||
|
||||
# Check if we need to import users as well
|
||||
if len(users):
|
||||
# For all invited users create the uers
|
||||
# For all invited users create the users
|
||||
new_users = User.objects.bulk_create(
|
||||
[
|
||||
User(
|
||||
@@ -56,6 +57,15 @@ def service_importer(service, importer_id):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
[
|
||||
send_welcome_email.delay(
|
||||
str(user.id),
|
||||
True,
|
||||
f"{user.email} was imported to Plane from {service}",
|
||||
)
|
||||
for user in new_users
|
||||
]
|
||||
|
||||
workspace_users = User.objects.filter(
|
||||
email__in=[
|
||||
user.get("email").strip().lower()
|
||||
|
||||
@@ -506,119 +506,6 @@ def track_blockings(
|
||||
)
|
||||
|
||||
|
||||
def track_cycles(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_cycle_issues", [])
|
||||
created_records = json.loads(current_instance.get("created_cycle_issues", []))
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("old_cycle_id", None)
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("new_cycle_id", None)
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_cycle.name,
|
||||
new_value=new_cycle.name,
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
)
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
cycle = Cycle.objects.filter(
|
||||
pk=created_record.get("fields").get("cycle")
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=created_record.get("fields").get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=cycle.name,
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_modules(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
):
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_module_issues", [])
|
||||
created_records = json.loads(current_instance.get("created_module_issues", []))
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_module = Module.objects.filter(
|
||||
pk=updated_record.get("old_module_id", None)
|
||||
).first()
|
||||
new_module = Module.objects.filter(
|
||||
pk=updated_record.get("new_module_id", None)
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_module.name,
|
||||
new_value=new_module.name,
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
)
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
module = Module.objects.filter(
|
||||
pk=created_record.get("fields").get("module")
|
||||
).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=created_record.get("fields").get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=module.name,
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
@@ -683,8 +570,6 @@ def update_issue_activity(
|
||||
"assignees_list": track_assignees,
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
"cycles_list": track_cycles,
|
||||
"modules_list": track_modules,
|
||||
"estimate_point": track_estimate_points,
|
||||
}
|
||||
|
||||
@@ -788,6 +673,177 @@ def delete_comment_activity(
|
||||
)
|
||||
|
||||
|
||||
def create_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_cycle_issues", [])
|
||||
created_records = json.loads(current_instance.get("created_cycle_issues", []))
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("old_cycle_id", None)
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(
|
||||
pk=updated_record.get("new_cycle_id", None)
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_cycle.name,
|
||||
new_value=new_cycle.name,
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
)
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
cycle = Cycle.objects.filter(
|
||||
pk=created_record.get("fields").get("cycle")
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=created_record.get("fields").get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=cycle.name,
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
cycle_id = requested_data.get("cycle_id", "")
|
||||
cycle = Cycle.objects.filter(pk=cycle_id).first()
|
||||
issues = requested_data.get("issues")
|
||||
|
||||
for issue in issues:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=cycle.name if cycle is not None else "",
|
||||
new_value="",
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
|
||||
old_identifier=cycle.id if cycle is not None else None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_module_issues", [])
|
||||
created_records = json.loads(current_instance.get("created_module_issues", []))
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_module = Module.objects.filter(
|
||||
pk=updated_record.get("old_module_id", None)
|
||||
).first()
|
||||
new_module = Module.objects.filter(
|
||||
pk=updated_record.get("new_module_id", None)
|
||||
).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=old_module.name,
|
||||
new_value=new_module.name,
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
)
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
module = Module.objects.filter(
|
||||
pk=created_record.get("fields").get("module")
|
||||
).first()
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=created_record.get("fields").get("issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=module.name,
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
module_id = requested_data.get("module_id", "")
|
||||
module = Module.objects.filter(pk=module_id).first()
|
||||
issues = requested_data.get("issues")
|
||||
|
||||
for issue in issues:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=module.name if module is not None else "",
|
||||
new_value="",
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
|
||||
old_identifier=module.id if module is not None else None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
@@ -910,6 +966,10 @@ def issue_activity(
|
||||
"comment.activity.created": create_comment_activity,
|
||||
"comment.activity.updated": update_comment_activity,
|
||||
"comment.activity.deleted": delete_comment_activity,
|
||||
"cycle.activity.created": create_cycle_issue_activity,
|
||||
"cycle.activity.deleted": delete_cycle_issue_activity,
|
||||
"module.activity.created": create_module_issue_activity,
|
||||
"module.activity.deleted": delete_module_issue_activity,
|
||||
"link.activity.created": create_link_activity,
|
||||
"link.activity.updated": update_link_activity,
|
||||
"link.activity.deleted": delete_link_activity,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_welcome_email(user_id, created, message):
|
||||
try:
|
||||
instance = User.objects.get(pk=user_id)
|
||||
|
||||
if created and not instance.is_bot:
|
||||
first_name = instance.first_name.capitalize()
|
||||
to_email = instance.email
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
|
||||
subject = f"Welcome to Plane ✈️!"
|
||||
|
||||
context = {"first_name": first_name, "email": instance.email}
|
||||
|
||||
html_content = render_to_string(
|
||||
"emails/auth/user_welcome_email.html", context
|
||||
)
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, text_content, from_email_string, [to_email]
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
|
||||
# Send message on slack as well
|
||||
if settings.SLACK_BOT_TOKEN:
|
||||
client = WebClient(token=settings.SLACK_BOT_TOKEN)
|
||||
try:
|
||||
_ = client.chat_postMessage(
|
||||
channel="#trackers",
|
||||
text=message,
|
||||
)
|
||||
except SlackApiError as e:
|
||||
print(f"Got an error: {e.response['error']}")
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
||||
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 3.2.18 on 2023-05-01 19:56
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0028_auto_20230414_1703'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='view_props',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importer',
|
||||
name='imported_data',
|
||||
field=models.JSONField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='view_props',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SlackProjectSync',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('access_token', models.CharField(max_length=300)),
|
||||
('scopes', models.TextField()),
|
||||
('bot_user_id', models.CharField(max_length=50)),
|
||||
('webhook_url', models.URLField(max_length=1000)),
|
||||
('data', models.JSONField(default=dict)),
|
||||
('team_id', models.CharField(max_length=30)),
|
||||
('team_name', models.CharField(max_length=300)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_slackprojectsync', to='db.workspace')),
|
||||
('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Slack Project Sync',
|
||||
'verbose_name_plural': 'Slack Project Syncs',
|
||||
'db_table': 'slack_project_syncs',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('team_id', 'project')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -59,6 +59,7 @@ from .integration import (
|
||||
GithubRepositorySync,
|
||||
GithubIssueSync,
|
||||
GithubCommentSync,
|
||||
SlackProjectSync,
|
||||
)
|
||||
|
||||
from .importer import Importer
|
||||
|
||||
@@ -16,6 +16,7 @@ class Cycle(ProjectBaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="owned_by_cycle",
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
|
||||
@@ -33,6 +33,7 @@ class Importer(ProjectBaseModel):
|
||||
token = models.ForeignKey(
|
||||
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
|
||||
)
|
||||
imported_data = models.JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Importer"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from .base import Integration, WorkspaceIntegration
|
||||
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
|
||||
from .slack import SlackProjectSync
|
||||
@@ -0,0 +1,32 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import ProjectBaseModel
|
||||
|
||||
|
||||
class SlackProjectSync(ProjectBaseModel):
|
||||
access_token = models.CharField(max_length=300)
|
||||
scopes = models.TextField()
|
||||
bot_user_id = models.CharField(max_length=50)
|
||||
webhook_url = models.URLField(max_length=1000)
|
||||
data = models.JSONField(default=dict)
|
||||
team_id = models.CharField(max_length=30)
|
||||
team_name = models.CharField(max_length=300)
|
||||
workspace_integration = models.ForeignKey(
|
||||
"db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the repo name"""
|
||||
return f"{self.project.name}"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["team_id", "project"]
|
||||
verbose_name = "Slack Project Sync"
|
||||
verbose_name_plural = "Slack Project Syncs"
|
||||
db_table = "slack_project_syncs"
|
||||
ordering = ("-created_at",)
|
||||
@@ -39,6 +39,7 @@ class Module(ProjectBaseModel):
|
||||
through="ModuleMember",
|
||||
through_fields=("module", "member"),
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
|
||||
@@ -80,7 +80,7 @@ AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = ""
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
@@ -13,6 +13,17 @@ def filter_state(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
def filter_estimate_point(params, filter, method):
|
||||
if method == "GET":
|
||||
estimate_points = params.get("estimate_point").split(",")
|
||||
if len(estimate_points) and "" not in estimate_points:
|
||||
filter["estimate_point__in"] = estimate_points
|
||||
else:
|
||||
if params.get("estimate_point", None) and len(params.get("estimate_point")):
|
||||
filter["estimate_point__in"] = params.get("estimate_point")
|
||||
return filter
|
||||
|
||||
|
||||
def filter_priority(params, filter, method):
|
||||
if method == "GET":
|
||||
priorties = params.get("priority").split(",")
|
||||
@@ -192,6 +203,7 @@ def issue_filters(query_params, method):
|
||||
|
||||
ISSUE_FILTER = {
|
||||
"state": filter_state,
|
||||
"estimate_point": filter_estimate_point,
|
||||
"priority": filter_priority,
|
||||
"parent": filter_parent,
|
||||
"labels": filter_labels,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
# build
|
||||
RUN yarn build
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=builder /app/next.config.js .
|
||||
COPY --from=builder /app/package.json .
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
EXPOSE 3000
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CyclesIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.5 17.5H3.5V20.5M20.5 20.5H17.5V17.5M17.5 6.5H20.5V3.5M3.5 3.5H6.5V6.5"
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 3.647C3.789 5.4355 2 8.509 2 12C2 12.51 2.038 13.0105 2.1115 13.5M13.5 21.888C13.0035 21.9626 12.5021 22.0001 12 22C8.509 22 5.4355 20.211 3.647 17.5M21.888 10.5C21.962 10.9895 22 11.49 22 12C22 15.491 20.211 18.5645 17.5 20.353M10.5 2.1115C10.9965 2.03703 11.4979 1.99976 12 2C15.491 2 18.5645 3.789 20.353 6.5"
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const TargetIcon: React.FC<Props> = ({
|
||||
width = "24",
|
||||
height = "24",
|
||||
className,
|
||||
color = "black",
|
||||
}) => (
|
||||
<svg width={width} height={height} className={className} color={color} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xlinkHref="http://www.w3.org/1999/xlink">
|
||||
<rect width="16" height="16" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlinkHref="#image0_2094_50417" transform="scale(0.01)"/>
|
||||
</pattern>
|
||||
<image id="image0_2094_50417" width="100" height="100" xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMyElEQVR4nO1de3AdZRX/3TQpBdJH2gZR+zCJUHDUOoiKSO2DWhkZRjttaauAPEpQRKWog9Q+RYGqbcdhUOlgKeXhCNVIq/KHTxCfQSwtIAVtG8ACaRraatskTe51DnOusx7O3nu+b3fv3Zu5v5mdySQ5u99+5/vO+zsLVFFFFVVUUcXgxTAAkwFcCGApgHsB/BHAdgD/BNANoBdAP//8IoB/APgdgE0AVgG4FMC7ANSW+2UqEXUAzgGwHMBvAPQAyMV0/QfAIwBuAfBBAEPK/bJpRQ2AcwHcBeBQjAwodu0DsBHA+TEzZyUqFOMB3ATg+RIyIRdyPc8TSWOKgpV8v4rCWwHcwbLfMlm7AfwUwLcAXAlgKoC3A2gC0MBibgj//GYApwKYyf97M4CfAHjJ+KxjAO4E0BKBGRXDkCZWyv1FJuVfrJBJGU+I8flvAfBxAPcAeNXImAkezEg9Q45jC+lIgQn4N8vzGaxTkkYd76KNRcZ1GMANAIY6MCPVDKGX3lnghelvlwM4sYxjJFF3LYDnCozzGbbMLMxIJUNqebADIQMmP+KSlJmeNQDmAXg2ZMxZAN8O7JYwZqSOIRMB/ClkoC8DuBhABulFHYDPFNAz5JiuLaKDUoPz2GOWAyRFfhuAUagcnMwGQNwmdsn8lIsA9CkD2APgLFQuzgfQWWnM+GyIvngQwGhUPk4C8FBEZqwo1WC/GiKiFqdcV/go/dTvjMXKwykgOCeBFTqHV9kPAPwVwC7WV318dbN4/AuA+3ihfBTA2BjHkXqdkRUPPwhgekz3P5OtmR3Kc1wuon2K42YUyo+C1DLjPEWBH+RJjILhAK7jCcwldD0J4CoAJ3iML5XMmKiYtj0Rd0YDv8D+BBkhry4ASwAcbxzj/DQyo05x+voj6AxS+pcZzMp+fi5FfBcBmCKivbX880QOcVA4Zh3rk2NF7v0Ci99izDiWNmaAJ0QOgBS7D1o41Rr2Yr0A2pjZIyKMeQRHeduKhPwpxP+mSmLGTEW5tnmatnMBHCggSpbFbB3l8UYAN3K2UHs2icyPFGHGMf592UPoO5XEEYkJFxDzvlkg570EQD2Sx0i2urTQOzm41wNYEJEZIzn0QnMXO5YqMv39jvcgWX93CDMeZPlfajRzMYVFHLkyI69rae5iRZOykihQ6MqMLcpLHgVwNcqLDOvBYwkwI5/kinWx3aeE0Ec5vvDdIanaM5AeTA2JVLvqjDHKfSgdHFtBgsyBUz7DBZrOeJZ3XpowP0YF/nnlPqfEMcg7xI23OVpVc0OY8QakC/NjtqbqlMwjFW5EwnjFbqeSThc/44AiptK2M+YpzCCp8ImI971M3LOPy5S8cZO44U6HHHhGcfqOxqAzWtgIIL32OPssfaw4d3N6dSN73pTxi3NnNPMqzzjskl1x5URqlIpCCkf4ro5cBGtqCICFAH5vNE/zFzmxvwAwKyZmdPD/0LtZca0iIbyKvs9V6qaspToNSmyK/AwfzOISnFzEi/In7/FkRlOAGXS94uC8NvDuDT4nbIEUxF3iJiQGrJDlMT52+PEA1sfACDnhN7KV6KLAT1FSDVSdb8VGQft9x7l4re5IVqFTRaE1nyFD6BQOcUEjgPYiq50mZBqA03jnnsBln1P5b486MqqYNXWroHnVYZd8WNB2F6mEfB3OUeSetbzzOkHb5RibagTw9xB98ACASQ73mswOaaFso9W0beQEXJCWCjusyl0uUq0KMhTLI9jPMtNHUVsXMdWuTNquiGVE00Oq3139jK8J+qcdaDcJWjrZZYYMtlEVugVnCrpexxD6emXSHo4hDB+X09es7LZ3G2kvFXQkUs1n+uQxMmtJ/lolV2LFrBBmOMnaEnjgcrFSWMiCiYKuh0WZSe4GCcnZsmKHoJ3j4Gc8o4iptOyMIC5RisateFHQvsNCdKEg2upQN5UVL05haAsWKgr8LESDq59hLdAYLd4zy1lIC34mxlMsj/8algkiyqFbMEfQ/Rl2SA/8AUSDjwf+S4f7SyuQivEsuFnQUWiqKGS1N53Ts2CFoFvjEJuSK24S/OEbDsk6BP42eBY2yHASmeTOq5UcLZ8kFpXqWHC14vSVSmf83HPxXSno7vc0XMhAKIrtgohOu1rwmKCjuikfRi6HW3pgM0cVuj0U+CrPrN4UQUfpWgveJugoX1IUuwWRNQa1R9DRiVcLHhd00xyYUajK0WJNTRc0tKgsaFIK7Sw4WdCRs1oUXYKI8sOlpDvVSLc5IjPy+kvWClgwUtBREs5asBeko1BMUcgModUxi4tuuJGuUAsOEl8W1CsJNAvqBF1fknSDgSEHjPcYrnjPFhxXSrpKEVltBRjyQ+M9Jgk6SqpZMFbQ7UuSrtxKfbrDZGo1VMTgccZ7zIhJqe9Kkq7UZu+9Ec3e+1k5HuSdYWWGltk0OWrsmwXp6MiDBe8UdE9YiB4tsWP46RgdQ1f8TTy71UjXKugoumGBNLOpKsc5dNLqmdRa45ljyEYMnVhxuhhv1kE8rxO01mLqT/kk/pZ5TmyU4KLclZuRPH4snkmt/6yQWc3Znoz8ik/4nU4VwZhzzorKP2v4fYGyWs9GcviAYgxQ9aK1rKdfnCWxWpQydkZlts6KxyVBtd3ngZygelp5LjE5bjQqFuEOhyIOefCTrEQrOnwMpmHssfqYvmsEHbXWczkul0sghRvEUBZNcjda42dQzrisdtCVQbojLierfi2IraWTZ0QscvhuCFPi2CmNCjNyXG8Fh6yoLJizNiK4QtC5JMRep9it9rmWV3fxK4axMZBTHKizI+oMKabo+oPj+b9VPn5EiL+1JIrS2+sgYxdHLJQbq+iTvGj5EZurVpyuWFPBbg5WZZyP1MrIAB3KserIlwWtU81AnRK8IxlvQb0Sn3I99Di2QEe6HDt0Kzn0cRo/s55/nsF/k06f3BkuzNC8+k6H4vNZSvDTuQL+TnETKr72FXmHHWJbQfH1nQKT6nNlWWe4HlNuUQwd6lRqhXS26VSaM2ZEOI4wikv2ffwZbRxxNKLZ4WhN5ZFRGpftdUgT1PP5e59w1P+hRrGbyVLwLShzKU7WxjKfYz8ubZqybFnNi9ALWB64yXGbDnjGvXZHaer2dSUp73Kk7WElIRO1fVMTt1a6hyPMXWxe93J+4TG2Cls9xKTEe5UE2q8c6Gv50xlBejqf4o1xyoAozGHFBKUQodMhCVVONCsV892OTL5YWZCRDn1qFenbHbfcbEXMPJfCY9GyOkR2uaZ3uAB21CjmOzm+kdGi1Dt90vEesoQyzxRahWlDiyJmfETNIkHfF4MI/R9kW4xXHLsAZZRzdnlrJapOiRPvUxy4/JlAF6kwRmn/FFtrDXBw8XDE7VfLwcacIld9ra+4kGFrSmtu1ubhxK1XXAaX1LIJN4iHDHjEl2pDdkreT2lCeURUWIPkDR7MmKI0lP5SEgMfqpThd3h0rc6wTtH8iSMcjLQ6XVEwgsMhWgOzLOuMjEfI5wUlZmY6KeXrNcuJ3OLp6HwspIwnx6byioQSVCdx1LbQs12sqTwyyoGcAdfTtj5YrbzEFz3vRVbHb0MmJseWyRZOK0f5qkIDe/pbQz4WEHT6fD+x9GXlfuRYJ4465QzJgENOWltZFxk+2tXPxQXr2AufymbzaBanQ/nnZo5ZtfL/thu+ebXXMRwisUDRG4+U8kOWmgfew/1RfDGSw/Rh3UKTuDo5URRFZ81UrLN9SVhVxfAhZSCHRIMXH9SzCSoLJuK8nuDk0okxxLpk3qjXoQ1J7FiobNVDEXdKEPT92m9wsinsW1aWa4CrQ1bH0Iw/uDMOKc8pez/fzykT0OPYec5qUs4OfKC4nT9OvD8Q7d3Pv2vnaPBSpnHNDlp0huZEXoOUQPta2QA7RIPpgy4Ztqa03erS06UkuCZkoFsTWKHlwBiOJsj3I7/sC0gpFoZs5Q6uZKlUTFE88LwCL7vOsFhfXSEi7HsV9oGwMRwo1Hb+vnJaU64YX6CjWydXQpbie7e+qOF8hraw8k5fyf2MpD+9+iQXQpTMm3X49OpTRT69mliwsBSYFlKJGMwcLirRJyrCUM/tMrRMYXABJR4oLBXq2DMudJT5KJ8XvKBEu6aGe0vezgmksHEd5p0eZxV+ajCBKyCLfT7oJXbsLo8zD80JsCvYsdRStcGrj9OuFacrfCfmdqWVYC7k6uDTR2u5JmsaHywKi/ZO5gOWVzHNQ0rRX9jVw+npOBdCxWAcd/q0TlYuwWsPZwkj100NBtTwqt+g9MZN8jrAFSVTB1mIJ1YM4dar13MjfS3n7XsdYf/oFo7WDkpFnTSGsr6Yy8eIN/GkbisQ7d3G/7OJE1BzuQNolQFVVFFFFVVgsOK/CQ2naFV2iSEAAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./select-lead";
|
||||
export * from "./select-members";
|
||||
export * from "./select-status";
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
type?: "button" | "submit" | "reset";
|
||||
className?: string;
|
||||
theme?: "primary" | "secondary" | "success" | "danger";
|
||||
size?: "sm" | "rg" | "md" | "lg";
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const OutlineButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
onClick,
|
||||
type = "button",
|
||||
size = "sm",
|
||||
className = "",
|
||||
theme = "primary",
|
||||
disabled = false,
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<button
|
||||
type={type}
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`inline-flex items-center justify-center rounded font-medium duration-300 ${
|
||||
theme === "primary"
|
||||
? `${
|
||||
disabled ? "opacity-70" : ""
|
||||
} border border-brand-accent text-white shadow-sm hover:bg-brand-accent focus:outline-none`
|
||||
: theme === "secondary"
|
||||
? "border bg-transparent hover:bg-brand-surface-1"
|
||||
: theme === "success"
|
||||
? `${
|
||||
disabled ? "opacity-70" : ""
|
||||
} border border-transparent bg-green-500 text-white shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500`
|
||||
: `${
|
||||
disabled ? "opacity-70" : ""
|
||||
} border border-red-500 text-red-500 shadow-sm hover:bg-red-500 hover:text-white focus:outline-none focus:ring-2 focus:ring-red-500`
|
||||
} ${
|
||||
size === "sm"
|
||||
? "p-2 text-xs"
|
||||
: size === "md"
|
||||
? "text-md px-3 py-2"
|
||||
: size === "lg"
|
||||
? "text-md px-4 py-2"
|
||||
: "px-2.5 py-2 text-sm"
|
||||
} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
|
||||
OutlineButton.displayName = "Button";
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
type Props = {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ToggleSwitch: React.FC<Props> = (props) => {
|
||||
const { value, onChange, label, disabled, className } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
value ? "bg-green-500" : "bg-gray-200"
|
||||
} ${className || " "}`}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
value ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
@@ -1,272 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { SecondaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { ContrastIcon, PeopleGroupIcon, ViewListIcon } from "components/icons";
|
||||
import { DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const FeaturesSettings: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleSubmit = async (formData: Partial<IProject>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<IProject>(
|
||||
PROJECT_DETAILS(projectId as string),
|
||||
(prevData) => ({ ...(prevData as IProject), ...formData }),
|
||||
false
|
||||
);
|
||||
|
||||
mutate<IProject[]>(
|
||||
PROJECTS_LIST(workspaceSlug as string),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => {
|
||||
if (p.id === projectId)
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
await projectService
|
||||
.updateProject(workspaceSlug as string, projectId as string, formData)
|
||||
.then((res) => {
|
||||
mutate(PROJECT_DETAILS(projectId as string));
|
||||
mutate(PROJECTS_LIST(workspaceSlug as string));
|
||||
setToastAlert({
|
||||
title: "Success!",
|
||||
type: "success",
|
||||
message: "Project features updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${projectDetails?.name ?? "Project"}`}
|
||||
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||
/>
|
||||
<BreadcrumbItem title="Features Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-2xl font-semibold">Features</h3>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-surface-1 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<ContrastIcon color="#3F76FF" width={28} height={28} className="flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-xl font-semibold">Cycles</h4>
|
||||
<p className="text-brand-secondary">
|
||||
Cycles are enabled for all the projects in this workspace. Access them from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.cycle_view ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.cycle_view}
|
||||
onClick={() => {
|
||||
trackEventServices.trackMiscellaneousEvent(
|
||||
{
|
||||
workspaceId: (projectDetails?.workspace as any)?.id,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
projectIdentifier: projectDetails?.identifier,
|
||||
projectName: projectDetails?.name,
|
||||
},
|
||||
!projectDetails?.cycle_view ? "TOGGLE_CYCLE_ON" : "TOGGLE_CYCLE_OFF"
|
||||
);
|
||||
handleSubmit({ cycle_view: !projectDetails?.cycle_view });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Use cycles</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-brand-surface-1 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.cycle_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-surface-1 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<PeopleGroupIcon color="#FF6B00" width={28} height={28} className="flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="-mt-1.5 text-xl font-semibold">Modules</h4>
|
||||
<p className="text-brand-secondary">
|
||||
Modules are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.module_view ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.module_view}
|
||||
onClick={() => {
|
||||
trackEventServices.trackMiscellaneousEvent(
|
||||
{
|
||||
workspaceId: (projectDetails?.workspace as any)?.id,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
projectIdentifier: projectDetails?.identifier,
|
||||
projectName: projectDetails?.name,
|
||||
},
|
||||
!projectDetails?.module_view ? "TOGGLE_MODULE_ON" : "TOGGLE_MODULE_OFF"
|
||||
);
|
||||
handleSubmit({ module_view: !projectDetails?.module_view });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Use cycles</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-brand-surface-1 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.module_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-surface-1 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<ViewListIcon color="#05C3FF" width={28} height={28} className="flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="-mt-1.5 text-xl font-semibold">Views</h4>
|
||||
<p className="text-brand-secondary">
|
||||
Views are enabled for all the projects in this workspace. Access it from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.issue_views_view ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.issue_views_view}
|
||||
onClick={() => {
|
||||
trackEventServices.trackMiscellaneousEvent(
|
||||
{
|
||||
workspaceId: (projectDetails?.workspace as any)?.id,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
projectIdentifier: projectDetails?.identifier,
|
||||
projectName: projectDetails?.name,
|
||||
},
|
||||
!projectDetails?.issue_views_view ? "TOGGLE_VIEW_ON" : "TOGGLE_VIEW_OFF"
|
||||
);
|
||||
handleSubmit({ issue_views_view: !projectDetails?.issue_views_view });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Use views</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-brand-surface-1 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.issue_views_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-brand-base bg-brand-surface-1 p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<DocumentTextIcon color="#FCBE1D" width={28} height={28} className="flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-xl font-semibold">Pages</h4>
|
||||
<p className="text-brand-secondary">
|
||||
Pages are enabled for all the projects in this workspace. Access them from the
|
||||
navigation bar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
projectDetails?.page_view ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={projectDetails?.page_view}
|
||||
onClick={() => {
|
||||
trackEventServices.trackMiscellaneousEvent(
|
||||
{
|
||||
workspaceId: (projectDetails?.workspace as any)?.id,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
projectIdentifier: projectDetails?.identifier,
|
||||
projectName: projectDetails?.name,
|
||||
},
|
||||
!projectDetails?.page_view ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF"
|
||||
);
|
||||
handleSubmit({ page_view: !projectDetails?.page_view });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Use cycles</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-brand-surface-1 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
projectDetails?.page_view ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
||||
<SecondaryButton outline>Plane is open-source, view Roadmap</SecondaryButton>
|
||||
</a>
|
||||
<a href="https://github.com/makeplane/plane" target="_blank" rel="noreferrer">
|
||||
<SecondaryButton outline>Star us on GitHub</SecondaryButton>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesSettings;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
// services
|
||||
import appinstallationsService from "services/app-installations.service";
|
||||
// components
|
||||
import { Spinner } from "components/ui";
|
||||
|
||||
interface IGithuPostInstallationProps {
|
||||
installation_id: string;
|
||||
setup_action: string;
|
||||
state: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
// TODO:Change getServerSideProps to router.query
|
||||
const AppPostInstallation = ({
|
||||
installation_id,
|
||||
setup_action,
|
||||
state,
|
||||
provider,
|
||||
}: IGithuPostInstallationProps) => {
|
||||
useEffect(() => {
|
||||
if (state && installation_id) {
|
||||
appinstallationsService
|
||||
.addGithubApp(state, provider, { installation_id })
|
||||
.then((res) => {
|
||||
window.opener = null;
|
||||
window.open("", "_self");
|
||||
window.close();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}, [state, installation_id, provider]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 z-50 flex h-full w-full flex-col items-center justify-center gap-y-3 bg-brand-surface-2">
|
||||
<h2 className="text-2xl text-brand-base">Installing. Please wait...</h2>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: context.query,
|
||||
};
|
||||
}
|
||||
|
||||
export default AppPostInstallation;
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 37 KiB |
@@ -1,20 +0,0 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
class AppInstallationsService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
}
|
||||
|
||||
async addGithubApp(workspaceSlug: string, provider: string, data: any): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/workspace-integrations/${provider}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AppInstallationsService();
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
// All the app integrations that are available
|
||||
export interface IAppIntegration {
|
||||
author: string;
|
||||
author: "";
|
||||
avatar_url: string | null;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
description: any;
|
||||
id: string;
|
||||
metadata: any;
|
||||
network: number;
|
||||
provider: string;
|
||||
redirect_url: string;
|
||||
title: string;
|
||||
updated_at: string;
|
||||
updated_by: string | null;
|
||||
verified: boolean;
|
||||
webhook_secret: string;
|
||||
webhook_url: string;
|
||||
}
|
||||
|
||||
export interface IWorkspaceIntegration {
|
||||
actor: string;
|
||||
api_token: string;
|
||||
config: any;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
id: string;
|
||||
integration: string;
|
||||
integration_detail: IIntegrations;
|
||||
metadata: any;
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
}
|
||||
-8029
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -4,10 +4,9 @@ RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
CMD ["yarn","dev"]
|
||||
@@ -0,0 +1,72 @@
|
||||
FROM node:18-alpine AS builder
|
||||
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 . .
|
||||
|
||||
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
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
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
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
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 ./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
|
||||
|
||||
EXPOSE 3000
|
||||
+8
-5
@@ -92,13 +92,13 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
<>
|
||||
<form className="space-y-5 py-5 px-5">
|
||||
{(codeSent || codeResent) && (
|
||||
<div className="rounded-md bg-green-50 p-4">
|
||||
<div className="rounded-md bg-green-500/20 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-400" aria-hidden="true" />
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-green-800">
|
||||
<p className="text-sm font-medium text-green-500">
|
||||
{codeResent
|
||||
? "Please check your mail for new code."
|
||||
: "Please check your mail for code."}
|
||||
@@ -141,7 +141,9 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
<button
|
||||
type="button"
|
||||
className={`mt-5 flex w-full justify-end text-xs outline-none ${
|
||||
isResendDisabled ? "cursor-default text-gray-400" : "cursor-pointer text-brand-accent"
|
||||
isResendDisabled
|
||||
? "cursor-default text-brand-secondary"
|
||||
: "cursor-pointer text-brand-accent"
|
||||
} `}
|
||||
onClick={() => {
|
||||
setIsCodeResending(true);
|
||||
@@ -174,7 +176,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => {
|
||||
className="w-full text-center"
|
||||
size="md"
|
||||
onClick={handleSubmit(handleSignin)}
|
||||
loading={isSubmitting || (!isValid && isDirty)}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
+5
-2
@@ -94,7 +94,9 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="ml-auto text-sm">
|
||||
<Link href={"/forgot-password"}>
|
||||
<a className="font-medium text-brand-accent hover:text-indigo-500">Forgot your password?</a>
|
||||
<a className="font-medium text-brand-accent hover:text-brand-accent">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +104,8 @@ export const EmailPasswordForm = ({ onSuccess }: any) => {
|
||||
<SecondaryButton
|
||||
type="submit"
|
||||
className="w-full text-center"
|
||||
loading={isSubmitting || (!isValid && isDirty)}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign In"}
|
||||
</SecondaryButton>
|
||||
+2
-2
@@ -33,11 +33,11 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="px-1 w-full">
|
||||
<div className="w-full px-1">
|
||||
<Link
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-gray-600 duration-300 hover:bg-gray-50">
|
||||
<button className="flex w-full items-center justify-center gap-3 rounded-md border border-brand-base p-2 text-sm font-medium text-brand-secondary duration-300 hover:bg-brand-surface-2">
|
||||
<Image src={githubImage} height={22} width={22} color="#000" alt="GitHub Logo" />
|
||||
<span>Sign In with Github</span>
|
||||
</button>
|
||||
+1
-1
@@ -47,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<div className="h-12" id="googleSignInButton" ref={googleSignInButton} />
|
||||
<div className="overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
-6
@@ -36,16 +36,14 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
alt="ProjectSettingImg"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-xl font-medium text-brand-base">
|
||||
Oops! You are not authorized to view this page
|
||||
</h1>
|
||||
<h1 className="text-xl font-medium">Oops! You are not authorized to view this page</h1>
|
||||
|
||||
<div className="w-full text-base text-brand-secondary max-w-md ">
|
||||
<div className="w-full max-w-md text-base text-brand-secondary">
|
||||
{user ? (
|
||||
<p>
|
||||
You have signed in as {user.email}. <br />
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<a className="text-brand-base font-medium">Sign in</a>
|
||||
<a className="font-medium text-brand-base">Sign in</a>
|
||||
</Link>{" "}
|
||||
with different account that has access to this page.
|
||||
</p>
|
||||
@@ -53,7 +51,7 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
|
||||
<p>
|
||||
You need to{" "}
|
||||
<Link href={`/signin?next=${currentPath}`}>
|
||||
<a className="text-brand-base font-medium">Sign in</a>
|
||||
<a className="font-medium text-brand-base">Sign in</a>
|
||||
</Link>{" "}
|
||||
with an account that has access to this page.
|
||||
</p>
|
||||
+2
-2
@@ -45,9 +45,9 @@ export const JoinProject: React.FC = () => {
|
||||
<div className="h-44 w-72">
|
||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
||||
</div>
|
||||
<h1 className="text-xl font-medium text-gray-900">You are not a member of this project</h1>
|
||||
<h1 className="text-xl font-medium">You are not a member of this project</h1>
|
||||
|
||||
<div className="w-full max-w-md text-base text-gray-500 ">
|
||||
<div className="w-full max-w-md text-base text-brand-secondary">
|
||||
<p className="mx-auto w-full text-sm md:w-3/4">
|
||||
You are not a member of this project, but you can join this project by clicking the button
|
||||
below.
|
||||
+2
-2
@@ -20,12 +20,12 @@ export const NotAWorkspaceMember = () => {
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
||||
<p className="text-sm text-gray-500 w-1/2 mx-auto">
|
||||
<p className="mx-auto w-1/2 text-sm text-brand-secondary">
|
||||
You{"'"}re not a member of this workspace. Please contact the workspace admin to get
|
||||
an invitation or check your pending invitations.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Link href="/invitations">
|
||||
<a>
|
||||
<SecondaryButton>Check pending invites</SecondaryButton>
|
||||
@@ -17,7 +17,17 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-8 w-8 flex-shrink-0 cursor-pointer place-items-center rounded border border-brand-base text-center text-sm hover:bg-brand-surface-1"
|
||||
onClick={() => router.back()}
|
||||
onClick={() => {
|
||||
const lastTenUrls = JSON.parse(localStorage.getItem("lastTenUrls") || "[]");
|
||||
if (lastTenUrls.length > 0) {
|
||||
const url = lastTenUrls[1];
|
||||
lastTenUrls.splice(0, 2);
|
||||
localStorage.setItem("lastTenUrls", JSON.stringify(lastTenUrls));
|
||||
router.push(url);
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowLeftIcon className="h-3 w-3" />
|
||||
</button>
|
||||
+2
-2
@@ -14,7 +14,7 @@ import stateService from "services/state.service";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys";
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
||||
// icons
|
||||
import { CheckIcon, getStateGroupIcon } from "components/icons";
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
+2
-2
@@ -393,7 +393,7 @@ export const CommandPalette: React.FC = () => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl bg-brand-surface-2 border-brand-base border shadow-2xl transition-all">
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-brand-base divide-opacity-10 rounded-xl border border-brand-base bg-brand-surface-2 shadow-2xl transition-all">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
@@ -675,7 +675,7 @@ export const CommandPalette: React.FC = () => {
|
||||
|
||||
<Command.Group heading="Page">
|
||||
<Command.Item onSelect={createNewPage} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<div className="flex items-center gap-2 text-brand-secondary">
|
||||
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
|
||||
Create new page
|
||||
</div>
|
||||
+19
-9
@@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { MacCommandIcon } from "components/icons";
|
||||
import { CommandIcon } from "components/icons";
|
||||
// ui
|
||||
import { Input } from "components/ui";
|
||||
|
||||
@@ -123,17 +123,23 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<div key={shortcut.keys} className="flex w-full flex-col">
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-brand-secondary">{shortcut.description}</p>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
{shortcut.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2.5">
|
||||
{shortcut.keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-2">
|
||||
<MacCommandIcon />
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5">
|
||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</span>
|
||||
) : key === "Ctrl" ? (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</kbd>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-gray-800">
|
||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
||||
{key}
|
||||
</kbd>
|
||||
)}
|
||||
</span>
|
||||
@@ -167,12 +173,16 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
{keys.split(",").map((key, index) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base text-brand-secondary bg-brand-surface-1 p-2">
|
||||
<MacCommandIcon />
|
||||
<span className="flex h-full items-center rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-brand-secondary">
|
||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</span>
|
||||
) : key === "Ctrl" ? (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 p-1.5 text-sm font-medium text-brand-secondary">
|
||||
<CommandIcon className="h-4 w-4 fill-current text-brand-secondary" />
|
||||
</kbd>
|
||||
) : (
|
||||
<kbd className="rounded-sm border border-brand-base bg-brand-surface-1 px-2 py-1 text-sm font-medium text-brand-secondary">
|
||||
{key === "Ctrl" ? <MacCommandIcon /> : key}
|
||||
{key}
|
||||
</kbd>
|
||||
)}
|
||||
</span>
|
||||
+1
-1
@@ -17,7 +17,7 @@ type Props = {
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
removeIssue: ((bridgeId: string) => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
+3
-2
@@ -29,7 +29,7 @@ type Props = {
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
removeIssue: ((bridgeId: string) => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
@@ -130,7 +130,8 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
handleTrashBox={handleTrashBox}
|
||||
removeIssue={() => {
|
||||
if (removeIssue && issue.bridge_id) removeIssue(issue.bridge_id);
|
||||
if (removeIssue && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
userAuth={userAuth}
|
||||
+5
-5
@@ -349,7 +349,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm">
|
||||
<div className="flex flex-shrink-0 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>
|
||||
)}
|
||||
@@ -391,8 +391,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.link && (
|
||||
<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-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>
|
||||
@@ -401,8 +401,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.attachment_count && (
|
||||
<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-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-gray-500" />
|
||||
<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>
|
||||
+94
-81
@@ -26,7 +26,7 @@ import {
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
import { CustomMenu, Spinner, ToggleSwitch } from "components/ui";
|
||||
// icon
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -51,6 +51,7 @@ import { IIssue } from "types";
|
||||
// constant
|
||||
import { monthOptions, yearOptions } from "constants/calendar";
|
||||
import modulesService from "services/modules.service";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
addIssueToDate: (date: string) => void;
|
||||
@@ -62,9 +63,10 @@ interface ICalendarRange {
|
||||
}
|
||||
|
||||
export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
const [showWeekEnds, setShowWeekEnds] = useState<boolean>(false);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
const [isMonthlyView, setIsMonthlyView] = useState<boolean>(true);
|
||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
@@ -151,15 +153,15 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
||||
|
||||
const calendarIssues = cycleId
|
||||
? cycleCalendarIssues
|
||||
? (cycleCalendarIssues as IIssue[])
|
||||
: moduleId
|
||||
? moduleCalendarIssues
|
||||
: projectCalendarIssues;
|
||||
? (moduleCalendarIssues as IIssue[])
|
||||
: (projectCalendarIssues as IIssue[]);
|
||||
|
||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||
const filterIssue =
|
||||
calendarIssues && calendarIssues.length > 0
|
||||
? (calendarIssues as IIssue[]).filter(
|
||||
? calendarIssues.filter(
|
||||
(issue) =>
|
||||
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
|
||||
)
|
||||
@@ -324,7 +326,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-4 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none"
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none"
|
||||
onClick={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(new Date());
|
||||
@@ -337,14 +339,12 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Today{" "}
|
||||
Today
|
||||
</button>
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
className={`group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none `}
|
||||
>
|
||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base px-3 py-1 text-sm hover:bg-brand-surface-2 hover:text-brand-base focus:outline-none ">
|
||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
@@ -390,23 +390,10 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-brand-base py-2 px-1 text-sm text-brand-secondary">
|
||||
<h4>Show weekends</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showWeekEnds ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showWeekEnds}
|
||||
onClick={() => setShowWeekEnds(!showWeekEnds)}
|
||||
>
|
||||
<span className="sr-only">Show weekends</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showWeekEnds ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<ToggleSwitch
|
||||
value={showWeekEnds}
|
||||
onChange={() => setShowWeekEnds(!showWeekEnds)}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
@@ -445,61 +432,87 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||
} `}
|
||||
>
|
||||
{currentViewDaysData.map((date, index) => (
|
||||
<StrictModeDroppable droppableId={date.date}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={`group flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
|
||||
showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
{currentViewDaysData.map((date, index) => {
|
||||
const totalIssues = date.issues.length;
|
||||
|
||||
return (
|
||||
<StrictModeDroppable droppableId={date.date}>
|
||||
{(provided) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={`group relative flex flex-col gap-1.5 border-t border-brand-base p-2.5 text-left text-sm font-medium hover:bg-brand-surface-1 ${
|
||||
showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
? ""
|
||||
: "border-r"
|
||||
: (index + 1) % 5 === 0
|
||||
? ""
|
||||
: "border-r"
|
||||
: (index + 1) % 5 === 0
|
||||
? ""
|
||||
: "border-r"
|
||||
}`}
|
||||
>
|
||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
||||
{date.issues.length > 0 &&
|
||||
date.issues.map((issue: IIssue, index) => (
|
||||
<Draggable draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={`w-full cursor-pointer truncate rounded bg-brand-surface-2 p-1.5 hover:scale-105 ${
|
||||
snapshot.isDragging ? "shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}
|
||||
className="w-full"
|
||||
>
|
||||
{issue.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<div className="flex items-center justify-center p-1.5 text-sm text-brand-secondary opacity-0 group-hover:opacity-100">
|
||||
<button
|
||||
className="flex items-center justify-center gap-2 text-center"
|
||||
onClick={() => addIssueToDate(date.date)}
|
||||
}`}
|
||||
>
|
||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
||||
{totalIssues > 0 &&
|
||||
date.issues
|
||||
.slice(0, showAllIssues ? totalIssues : 4)
|
||||
.map((issue: IIssue, index) => (
|
||||
<Draggable draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={`w-full cursor-pointer truncate rounded border border-brand-base px-1.5 py-1 text-xs duration-300 hover:cursor-move hover:bg-brand-surface-2 ${
|
||||
snapshot.isDragging ? "bg-brand-surface-2 shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${issue?.project_detail.id}/issues/${issue.id}`}
|
||||
>
|
||||
<a className="flex w-full items-center gap-2">
|
||||
{getStateGroupIcon(
|
||||
issue.state_detail.group,
|
||||
"12",
|
||||
"12",
|
||||
issue.state_detail.color
|
||||
)}
|
||||
{issue.name}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 px-1.5 py-1 text-xs"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`absolute ${
|
||||
isMonthlyView ? "bottom-2" : "top-2"
|
||||
} right-2 flex items-center justify-center rounded-md bg-brand-surface-2 p-1 text-xs text-brand-secondary opacity-0 group-hover:opacity-100`}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-brand-secondary" />
|
||||
Add new issue
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center justify-center gap-1 text-center"
|
||||
onClick={() => addIssueToDate(date.date)}
|
||||
>
|
||||
<PlusIcon className="h-3 w-3 text-brand-secondary" />
|
||||
Add issue
|
||||
</button>
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
))}
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
+14
-13
@@ -117,7 +117,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
@@ -130,7 +130,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-brand-surface-2 shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-brand-base bg-brand-base shadow-2xl transition-all">
|
||||
<form>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -151,26 +151,26 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||
className="max-h-80 scroll-py-2 divide-y divide-brand-base overflow-y-auto"
|
||||
>
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 px-3 text-xs font-semibold text-brand-base">
|
||||
<h2 className="mb-2 px-3 text-xs font-medium text-brand-base">
|
||||
Select issues to add
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
<ul className="text-sm text-brand-base">
|
||||
{filteredIssues.map((issue) => (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
|
||||
active ? "bg-gray-900 bg-opacity-5 text-brand-base" : ""
|
||||
}`
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
|
||||
active ? "bg-brand-surface-2 text-brand-base" : ""
|
||||
} ${selected ? "text-brand-base" : ""}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
@@ -182,7 +182,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
<span className="flex-shrink-0 text-xs">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
{issue.name}
|
||||
@@ -194,10 +194,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-brand-secondary">
|
||||
<LayerDiagonalIcon height="52" width="52" />
|
||||
<h3 className="text-sm text-brand-secondary">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>.
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>
|
||||
.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
@@ -67,15 +67,19 @@ const activityDetails: {
|
||||
},
|
||||
name: {
|
||||
message: "set the name to",
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: (
|
||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
description: {
|
||||
message: "updated the description.",
|
||||
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />,
|
||||
icon: (
|
||||
<ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-brand-secondary" aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
estimate_point: {
|
||||
message: "set the estimate point to",
|
||||
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
|
||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: "set the due date to",
|
||||
@@ -91,7 +95,7 @@ const activityDetails: {
|
||||
},
|
||||
estimate: {
|
||||
message: "updated the estimate",
|
||||
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
|
||||
icon: <PlayIcon className="h-3 w-3 -rotate-90 text-gray-500" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: "updated the link",
|
||||
@@ -153,11 +157,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
) {
|
||||
const { workspace_detail, project, issue } = activity;
|
||||
value = (
|
||||
<span className="text-gray-600">
|
||||
<span className="text-brand-secondary">
|
||||
created{" "}
|
||||
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
||||
<a className="inline-flex items-center hover:underline">
|
||||
this issue. <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5 ml-1" />
|
||||
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
@@ -198,7 +202,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
|
||||
if (activity.field === "comment") {
|
||||
return (
|
||||
<div key={activity.id}>
|
||||
<div key={activity.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
@@ -219,7 +223,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-brand-surface-2 px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-gray-400"
|
||||
className="h-3.5 w-3.5 text-brand-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
@@ -242,9 +246,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
: activity.old_value
|
||||
}
|
||||
editable={false}
|
||||
onBlur={() => ({})}
|
||||
noBorder
|
||||
customClassName="text-xs bg-brand-surface-1"
|
||||
customClassName="text-xs border border-brand-base bg-brand-base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,7 +271,7 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-1 ring-white">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-brand-surface-2 ring-white">
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
@@ -15,7 +15,7 @@ import issuesService from "services/issues.service";
|
||||
import projectService from "services/project.service";
|
||||
import stateService from "services/state.service";
|
||||
// types
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
import { IIssueFilterOptions } from "types";
|
||||
|
||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
@@ -37,7 +37,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
@@ -59,7 +59,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
||||
>
|
||||
<span className="font-medium capitalize text-brand-secondary">
|
||||
<span className="capitalize text-brand-secondary">
|
||||
{replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||
@@ -75,7 +75,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<p
|
||||
key={state?.id}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium text-white"
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
|
||||
style={{
|
||||
color: state?.color,
|
||||
backgroundColor: `${state?.color}20`,
|
||||
@@ -122,16 +122,16 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
{filters.priority?.map((priority: any) => (
|
||||
<p
|
||||
key={priority}
|
||||
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium capitalize text-white ${
|
||||
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||
priority === "urgent"
|
||||
? "bg-red-100 text-red-600 hover:bg-red-100"
|
||||
? "bg-red-500/20 text-red-500"
|
||||
: priority === "high"
|
||||
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
|
||||
? "bg-orange-500/20 text-orange-500"
|
||||
: priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
|
||||
? "bg-yellow-500/20 text-yellow-500"
|
||||
: priority === "low"
|
||||
? "bg-green-100 text-green-500 hover:bg-green-100"
|
||||
: "bg-brand-surface-1 text-gray-700 hover:bg-brand-surface-1"
|
||||
? "bg-green-500/20 text-green-500"
|
||||
: "bg-brand-surface-1 text-brand-secondary"
|
||||
}`}
|
||||
>
|
||||
<span>{getPriorityIcon(priority)}</span>
|
||||
@@ -170,7 +170,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.first_name}</span>
|
||||
@@ -203,7 +203,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (key as keyof IIssueFilterOptions) === "created_by" ? (
|
||||
) : key === "created_by" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.created_by?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.member.id === memberId)?.member;
|
||||
@@ -211,7 +211,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
return (
|
||||
<div
|
||||
key={`${memberId}-${key}`}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-1 font-medium capitalize"
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-brand-surface-1 px-1 capitalize"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.first_name}</span>
|
||||
@@ -253,25 +253,20 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
const color = label.color !== "" ? label.color : "#0f172a";
|
||||
return (
|
||||
<div
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 font-medium"
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||
style={{
|
||||
background: `${color}33`, // add 20% opacity
|
||||
color: color,
|
||||
backgroundColor: `${color}20`, // add 20% opacity
|
||||
}}
|
||||
key={labelId}
|
||||
>
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: color,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
<span>{label.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
@@ -341,8 +336,8 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
}
|
||||
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<span className="font-medium">Clear all filters</span>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<span>Clear all filters</span>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
+2
-2
@@ -65,7 +65,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
return (
|
||||
<Popover className="relative z-[2]" ref={ref}>
|
||||
<Popover.Button
|
||||
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-gray-700"
|
||||
className="rounded-md border border-brand-base bg-brand-surface-2 px-2 py-1 text-xs text-brand-secondary"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{label}
|
||||
@@ -79,7 +79,7 @@ export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange })
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-brand-surface-2 shadow-lg">
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg">
|
||||
<div className="h-96 w-80 overflow-auto rounded border border-brand-base bg-brand-surface-2 p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
|
||||
<Tab.Group>
|
||||
<Tab.List as="span" className="inline-block rounded bg-brand-surface-2 p-1">
|
||||
+6
-19
@@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { CustomMenu, ToggleSwitch } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
@@ -213,23 +213,10 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showEmptyGroups ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEmptyGroups}
|
||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
>
|
||||
<span className="sr-only">Show empty groups</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<ToggleSwitch
|
||||
value={showEmptyGroups}
|
||||
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
@@ -259,7 +246,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-brand-accent bg-brand-accent text-brand-base"
|
||||
? "border-brand-accent bg-brand-accent text-white"
|
||||
: "border-brand-base"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
STATE_LIST,
|
||||
STATES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
// image
|
||||
|
||||
@@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
} = useIssuesView();
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
@@ -314,10 +314,26 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
const removeIssueFromCycle = useCallback(
|
||||
(bridgeId: string) => {
|
||||
(bridgeId: string, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
|
||||
mutate(
|
||||
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
|
||||
(prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
if (selectedGroup) {
|
||||
const filteredData: any = {};
|
||||
for (const key in prevData) {
|
||||
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||
}
|
||||
return filteredData;
|
||||
} else {
|
||||
const filteredData = prevData.filter((i: any) => i.id !== issueId);
|
||||
return filteredData;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.removeIssueFromCycle(
|
||||
@@ -326,6 +342,13 @@ export const IssuesView: React.FC<Props> = ({
|
||||
cycleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
message: "Issue removed successfully.",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
@@ -334,10 +357,26 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
const removeIssueFromModule = useCallback(
|
||||
(bridgeId: string) => {
|
||||
(bridgeId: string, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||
mutate(
|
||||
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
|
||||
(prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
if (selectedGroup) {
|
||||
const filteredData: any = {};
|
||||
for (const key in prevData) {
|
||||
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
|
||||
}
|
||||
return filteredData;
|
||||
} else {
|
||||
const filteredData = prevData.filter((item: any) => item.id !== issueId);
|
||||
return filteredData;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
modulesService
|
||||
.removeIssueFromModule(
|
||||
@@ -346,6 +385,13 @@ export const IssuesView: React.FC<Props> = ({
|
||||
moduleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
message: "Issue removed successfully.",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
@@ -426,7 +472,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
{areFiltersApplied && (
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
<div className={`${issueView === "list" ? "mt-4" : "my-4"} border-t border-brand-base`} />
|
||||
)}
|
||||
</>
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type Props = {
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string) => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
+24
-22
@@ -216,7 +216,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</a>
|
||||
</ContextMenu>
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 border-b border-brand-base bg-brand-base px-4 py-2.5 last:border-b-0"
|
||||
className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-base bg-brand-base last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
@@ -224,26 +224,28 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<span className="flex-shrink-0 text-xs text-brand-secondary">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
<div className="flex-grow cursor-pointer px-4 pt-2.5 md:py-2.5">
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
<Tooltip
|
||||
tooltipHeading="Issue ID"
|
||||
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
|
||||
>
|
||||
<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="text-[0.825rem] text-brand-base">
|
||||
{truncateText(issue.name, 50)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<span className="text-[0.825rem] text-brand-base">
|
||||
{truncateText(issue.name, 50)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
<div className="flex w-full flex-shrink flex-wrap items-center gap-2 px-4 pb-2.5 text-xs sm:w-auto md:px-0 md:py-2.5 md:pr-4">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
@@ -268,7 +270,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.sub_issue_count && (
|
||||
<div className="flex items-center gap-1 rounded-md border border-brand-base px-3 py-1 text-xs text-brand-secondary 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>
|
||||
)}
|
||||
@@ -311,8 +313,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.link && (
|
||||
<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-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>
|
||||
@@ -321,8 +323,8 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.attachment_count && (
|
||||
<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-gray-500">
|
||||
<PaperClipIcon className="h-3.5 w-3.5 -rotate-45 text-gray-500" />
|
||||
<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>
|
||||
+2
-2
@@ -37,7 +37,7 @@ type Props = {
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string) => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
isCompleted?: boolean;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
@@ -204,7 +204,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
makeIssueCopy={() => makeIssueCopy(issue)}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id);
|
||||
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
isCompleted={isCompleted}
|
||||
userAuth={userAuth}
|
||||
+2
-2
@@ -78,7 +78,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
return 2;
|
||||
|
||||
default:
|
||||
return 3;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
return (
|
||||
@@ -94,7 +94,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
return setTab("States");
|
||||
|
||||
default:
|
||||
return setTab("States");
|
||||
return setTab("Assignees");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user