Compare commits

...

87 Commits

Author SHA1 Message Date
anmolsinghbhatia
f0397ef2b5 feat: back redirection 2023-05-04 12:13:09 +05:30
pablohashescobar
a2825208b8 docs: update self hosting section in readme (#1002)
docs: update self hosting section in readme
2023-05-03 23:53:53 +05:30
pablohashescobar
c3387ba974 fix: environment variables for the services (#1001) 2023-05-03 21:31:06 +05:30
pablohashescobar
baa9c30449 fix: single docker file (#999) 2023-05-03 16:48:04 +05:30
pablohashescobar
849e2d658a dev:: update docker file for frontend (#998) 2023-05-03 16:39:13 +05:30
pablohashescobar
c7f1090914 fix: docker setup (#987)
* removing dependencies from .env

* dev: Passing the arguments from docker compose to DockerWeb in nextjs to define base environment variables

* dev: removed env from docker-compose and taking the env from shell

* dev: Updated docker file and used console in signin to test the env from docker

* dev: Docker setting env variables via shell

* removed env variables and args

* Update Dockerfile.web

* Update Dockerfile.web

* Update signin.tsx

* .

* .

* dev: Added BASE_URL from docker

* dev: Updated docker config

* dev: scripts for replacing variable during runtime

* dev: entrypoint script

* dev: update replace env script and update docker entrypoint command for frontend

* dev: update replace env script to not update process.env

* dev: update docker file to add missing variables as well

* fix: updated docker compose yml and web

* dev: create start script to run docker and update script for replacing variables

* dev: update setup script and env example script to create variables in the root of the project

* .

* dev: update docker compose hub

* dev: update docker compose hub command

* dev: update docker compose yml and env example

* dev: update docker compose hub

* dev: single docker

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-05-03 13:36:55 +05:30
vamsi
e3d43298df Merge branch 'stage-release' of https://github.com/makeplane/plane into stage-release 2023-05-03 00:57:04 +05:30
pablohashescobar
b36565298f Merge pull request #992 from makeplane/develop
promote: develop to stage-release
2023-05-03 00:55:14 +05:30
guru_sainath
b26e8bd956 update: UI for google auth button and loadin toggle in signin screen (#991) 2023-05-03 00:48:24 +05:30
pablohashescobar
e5703dbe70 Merge pull request #990 from makeplane/develop
promote: develop to stage-release
2023-05-02 23:37:25 +05:30
Anmol Singh Bhatia
498d6d2b02 style: onboarding logo and style (#989) 2023-05-02 23:31:36 +05:30
pablohashescobar
1d22817ede fix: import user invite emails (#986) 2023-05-02 20:09:54 +05:30
Anmol Singh Bhatia
483c49d0ff fix: ui and bug fixes (#980)
* style: sub issue theming

* style: shortcut modal theming

* style: blocking and blocked by modal theming

* fix: filter labels dropdown width fix

* style: display properties

* chore: workspace invite

* style: invite co workers theming

* style: create workspace theming

* style: attachment theming

* style: workspace sidebar theming

* style: issue property theming

* style: create module modal lead icon

* style: label list modal theming

* delete attachment and member modal theming

* style: transfer issue modal

* style: delete estimate modal theming

* style: module form status

* style: delete state modal theming

* style: shortcut modal

* style: onboarding logo

* style: onboarding command menu
2023-05-02 20:00:33 +05:30
guru_sainath
0fa9451633 fix: workspace integration github repos issue resolved (#988) 2023-05-02 18:15:24 +05:30
pablohashescobar
46237c5431 Merge pull request #866 from makeplane/develop
promote: develop to stage-release
2023-05-02 01:57:04 +05:30
vamsi
20e400487f dev: migrations SlackProjectSync; new attributes to cycle, importer and module 2023-05-02 01:55:15 +05:30
pablohashescobar
99fb3c9bfe chore: log cycle delete and cycle issue delete activities (#975)
* chore: log cycle delete and cycle issue delete activities

* dev: remove print logs
2023-05-02 01:21:48 +05:30
pablohashescobar
88200a93bf feat: delete imported data as well when the import is deleted (#974) 2023-05-02 00:53:08 +05:30
pablohashescobar
b2ad071608 fix: bulk modules importer (#973) 2023-05-02 00:52:52 +05:30
pablohashescobar
21992f540f chore: importer user invite email (#972) 2023-05-02 00:52:36 +05:30
pablohashescobar
742731cbe6 chore: log module delete and module issue delete activities (#976) 2023-05-02 00:51:04 +05:30
pablohashescobar
c6878b9b0f fix: page access update (#977) 2023-05-02 00:50:41 +05:30
pablohashescobar
887cac5612 feat: filter issues by estimate points (#978) 2023-05-02 00:50:10 +05:30
guru_sainath
982566b5b4 Merge pull request #982 from makeplane/fix/slack-integration
fix: slack with project integration bug fixes
2023-05-02 00:47:36 +05:30
gurusainath
2dc5655886 fix: slack with project integration bug fixes 2023-05-02 00:40:47 +05:30
guru_sainath
160b4a4390 Merge pull request #970 from makeplane/refactor/estimates_modal
refactor: estimates payload generate function
2023-04-29 14:17:29 +05:30
guru_sainath
f187d9512a Merge pull request #969 from makeplane/style/workspace_sidebar
style: workspace sidebar
2023-04-29 14:15:41 +05:30
Aaryan Khandelwal
53d3ea1979 refactor: estimates payload generate function 2023-04-29 03:27:03 +05:30
anmolsinghbhatia
13d76d4325 style: workspace sidebar 2023-04-28 20:51:47 +05:30
guru_sainath
25338cc804 Merge pull request #968 from makeplane/style/onboarding_screens
style: onboarding screens, invitations page
2023-04-28 20:50:40 +05:30
Aaryan Khandelwal
eaa750b025 fix: initial onboarding step 2023-04-28 20:47:29 +05:30
Aaryan Khandelwal
81d9c70026 style: onboarding screens, invitations page 2023-04-28 20:46:07 +05:30
guru_sainath
93fb4fe1e9 Merge pull request #959 from makeplane/feat/product_update_modal
feat: product update modal
2023-04-28 19:38:33 +05:30
guru_sainath
b8e6d072cc Merge pull request #967 from makeplane/style/ui_fixes
style: command k item and cycle icon theming
2023-04-28 19:03:16 +05:30
anmolsinghbhatia
1220cebe50 style: command k item and cycle icon theming 2023-04-28 18:52:53 +05:30
anmolsinghbhatia
9464b5c00e style: product update modal 2023-04-28 18:29:01 +05:30
anmolsinghbhatia
3fae0f39c0 chore: product update interface 2023-04-28 18:28:43 +05:30
Kunal Vishwakarma
73a757e337 fix: added the decoded url for slack integration popup (#958) 2023-04-28 17:55:57 +05:30
Kunal Vishwakarma
bb40b7feb5 chore: removed email span from invite member page (#956) 2023-04-28 17:49:38 +05:30
Aaryan Khandelwal
3175ce9136 style: create project modal (#957) 2023-04-28 17:49:29 +05:30
Anmol Singh Bhatia
0b9b4bb289 fix: cycle & module mutation , feat: cycle & module toast alerts (#962)
* fix: cycle remove issue mutation

* fix: module remove issue mutation

* feat: issue removed toast alert added
2023-04-28 17:49:16 +05:30
Anmol Singh Bhatia
f0f24b6fc4 style: module , cycle & icon styling (#963)
* style: target icon updated

* style: cycle card theming

* style: module card theming

* style: no current cycle message
2023-04-28 17:49:04 +05:30
Anmol Singh Bhatia
429dffb055 fix: cycle & module sidebar default tab fix (#964) 2023-04-28 17:48:40 +05:30
guru_sainath
6e5c85cd6e Merge pull request #961 from makeplane/style/issue_list_responsive
style: responsive design for issue list in 'list' view
2023-04-28 10:49:54 +05:30
Dakshesh Jain
2adcb163fb Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 19:44:19 +05:30
Dakshesh Jain
028a350cd1 style: issue list dropdown on next line 2023-04-25 19:43:52 +05:30
Anmol Singh Bhatia
d021a5696a fix: sub issues list fix (#960) 2023-04-25 19:00:20 +05:30
Dakshesh Jain
a5c18e37c1 Merge branch 'develop' of https://github.com/makeplane/plane into style/issue_list_responsive 2023-04-25 18:36:24 +05:30
Dakshesh Jain
8e611664a8 style: made issue list view responsive 2023-04-25 18:35:58 +05:30
anmolsinghbhatia
3480b450f2 fix: build fix 2023-04-25 18:10:23 +05:30
anmolsinghbhatia
a7d9591c44 feat: product updates tag and date added 2023-04-25 18:02:13 +05:30
anmolsinghbhatia
1364c842e0 feat: product updates button added 2023-04-25 17:33:53 +05:30
anmolsinghbhatia
eb99b4adc9 feat: markdown to custom component 2023-04-25 16:54:12 +05:30
anmolsinghbhatia
6684dd4ab6 feat: product updates service added 2023-04-25 16:51:50 +05:30
Kunal Vishwakarma
529ed4432c chore: fixed sidebar highlight not working (#952) 2023-04-25 12:10:05 +05:30
Kunal Vishwakarma
8c1ad69f0c chore: changed pages empty state image (#954) 2023-04-25 12:09:29 +05:30
Kunal Vishwakarma
c23de32b03 chore: added create label inside of pages label select (#944)
* chore: added create label inside of pages label select

* chore: used footer instead of props
2023-04-25 12:08:56 +05:30
Rhea Jain
83ac1f4e4c chore: invite text update (#955) 2023-04-25 12:07:02 +05:30
Anmol Singh Bhatia
cee9695a4a feat: sub issues progress (#895) 2023-04-25 11:38:13 +05:30
Aaryan Khandelwal
c9f866e538 style: profile settings, activity theming (#951) 2023-04-24 18:53:30 +05:30
Aaryan Khandelwal
7d96adcb70 chore: empty state for estimates (#950) 2023-04-24 18:53:18 +05:30
Aaryan Khandelwal
c5b034385f chore: added custom toggle switch everywhere (#949) 2023-04-24 18:53:07 +05:30
Aaryan Khandelwal
ff7f31c35b style: calendar view (#948)
* style: calendar view

* chore: add issue button positioning
2023-04-24 18:10:44 +05:30
Aaryan Khandelwal
7234d6f68b style: workspace and project settings (#946) 2023-04-24 13:21:09 +05:30
Aaryan Khandelwal
d8a5b8d848 style: imports theming (#945) 2023-04-24 13:20:41 +05:30
Aaryan Khandelwal
5412e09701 chore: empty input fields text (#943) 2023-04-24 11:28:05 +05:30
Aaryan Khandelwal
7116acc331 style: auth pages theming (#942) 2023-04-24 11:20:14 +05:30
Aaryan Khandelwal
ae26b17cab style: filters list theming (#941) 2023-04-24 11:20:02 +05:30
Aaryan Khandelwal
2ec8fbab34 style: modals theming (#940)
* style: existing issues list modal

* style: parent issues list modal

* style: issue modal

* style: cycle modal

* style: module modal

* style: view modal

* style: page modal

* style: delete modals
2023-04-24 11:19:53 +05:30
Aaryan Khandelwal
213dc3f8e8 style: signin page theming (#938) 2023-04-24 11:19:43 +05:30
Aaryan Khandelwal
4b02886c40 chore: remove outline button component (#937) 2023-04-23 01:43:58 +05:30
Aaryan Khandelwal
8eddc4b304 fix: points not updating while editing estimate (#935) 2023-04-23 00:00:46 +05:30
Aaryan Khandelwal
169a60723b style: project settings theming (#936)
* style: project and workspace members theming

* style: project features theming

* style: project settings states theming

* style: project settings labels theming

* style: project settings integrations theming
2023-04-22 23:46:19 +05:30
Kunal Vishwakarma
c80094581e feat: frontend slack integration (#932)
* feat: slack integration frontend

* feat: slack integration frontend complete

* Co-authored-by: Aaryan Khandelwal <aaryan610@users.noreply.github.com>
2023-04-22 21:54:50 +05:30
pablohashescobar
d99f669b89 dev: add s3 url for staging (#934) 2023-04-22 18:21:22 +05:30
Aaryan Khandelwal
99dd1b9f0c chore: state delete validation (#930) 2023-04-22 18:19:35 +05:30
pablohashescobar
48e77ea81b remove: state delete issue validation endpoint (#929) 2023-04-22 18:18:08 +05:30
pablohashescobar
0be6738715 dev: back migration for integrations (#933) 2023-04-22 18:17:53 +05:30
Anmol Singh Bhatia
d041d8be6b fix: cycle date validation (#922) 2023-04-22 18:17:17 +05:30
pablohashescobar
e53847c59e feat: module view props (#882) 2023-04-22 18:16:16 +05:30
pablohashescobar
16781a71fe feat: cycle view props (#881) 2023-04-22 18:16:05 +05:30
pablohashescobar
fb4535b294 feat: slack integration (#874)
* feat: init slack integration

* dev: create model and update existing view for slack

* dev: update slack sync model and create view to install slack

* dev: workspace integration query

* dev: update the metadata validation for access_token and team_id and save config to database

* dev: update validation for team_id

* dev: update validation

* dev: update validations

* dev: remove bot access token field from sync

* dev: handle integrity exception
2023-04-22 18:15:52 +05:30
Vamsi Kurama
d2a58bf04a Merge pull request #862 from makeplane/stage-release
promote: staging to production v0.5
2023-04-17 19:08:10 +05:30
Vamsi Kurama
c51407c85e Merge branch 'master' into stage-release 2023-04-17 19:07:33 +05:30
Vamsi Kurama
3817511024 Merge pull request #824 from makeplane/feat/session_recorder
feat: session recorder
2023-04-14 16:09:31 +05:30
Aaryan Khandelwal
2950877767 feat: session recorder integrated 2023-04-14 15:27:14 +05:30
Vamsi Kurama
3d6f2dd3dc Merge pull request #703 from makeplane/stage-release
promote: stage-release to production
2023-04-04 19:38:37 +05:30
223 changed files with 3851 additions and 1928 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:xyzzyspoon@db:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMTP
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT="587"
EMAIL_USE_TLS="1"
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
# AWS
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_BUCKET_NAME=""
AWS_S3_ENDPOINT_URL=""
# FE
WEB_URL="localhost/"
# OAUTH
GITHUB_CLIENT_SECRET=""
# Flags
DISABLE_COLLECTSTATIC=1
DOCKERIZED=1
# GPT Envs
OPENAI_API_KEY=0
GPT_ENGINE=0

View File

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

View File

@@ -62,6 +62,7 @@ from .integration import (
GithubRepositorySerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
SlackProjectSyncSerializer,
)
from .importer import ImporterSerializer

View File

@@ -5,3 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer

View File

@@ -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",
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,3 +6,4 @@ from .github import (
GithubCommentSyncViewSet,
GithubRepositoriesEndpoint,
)
from .slack import SlackProjectSyncViewSet

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')},
},
),
]

View File

@@ -59,6 +59,7 @@ from .integration import (
GithubRepositorySync,
GithubIssueSync,
GithubCommentSync,
SlackProjectSync,
)
from .importer import Importer

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync

View File

@@ -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",)

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
@@ -12,10 +13,10 @@ RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
@@ -26,9 +27,17 @@ RUN yarn install
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM node:18-alpine AS runner
WORKDIR /app
@@ -43,8 +52,20 @@ COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
# COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone/node_modules ./apps/app/node_modules
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
ENV NEXT_TELEMETRY_DISABLED 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`} />
)}
</>

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const [showNoCurrentCycleMessage, setShowNoCurrentCycleMessage] = useState(true);
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
@@ -61,14 +60,9 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
))}
</div>
) : type === "current" ? (
showNoCurrentCycleMessage && (
<div className="flex items-center justify-between bg-brand-surface-2 w-full px-6 py-4 rounded-[10px]">
<h3 className="text-base font-medium text-brand-base "> No current cycle is present.</h3>
<button onClick={() => setShowNoCurrentCycleMessage(false)}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
)
<div className="flex w-full items-center justify-start rounded-[10px] bg-brand-surface-2 px-6 py-4">
<h3 className="text-base font-medium text-brand-base ">No current cycle is present.</h3>
</div>
) : (
<EmptyState
type="cycle"

View File

@@ -139,7 +139,7 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -153,30 +153,36 @@ export const DeleteCycleModal: React.FC<TConfirmCycleDeletionProps> = ({
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 transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-brand-surface-2 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-brand-base">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-brand-base"
>
Delete Cycle
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-brand-secondary">
Are you sure you want to delete cycle-{" "}
<span className="font-bold">{data?.name}</span>? All of the data related
to the cycle will be permanently removed. This action cannot be undone.
<span className="break-all font-medium text-brand-base">
{data?.name}
</span>
? All of the data related to the cycle will be permanently removed. This
action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
<div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}

View File

@@ -11,7 +11,11 @@ import useToast from "hooks/use-toast";
// ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { getDateRangeStatus, isDateRangeValid } from "helpers/date-time.helper";
import {
getDateRangeStatus,
isDateGreaterThanToday,
isDateRangeValid,
} from "helpers/date-time.helper";
// types
import { ICycle } from "types";
@@ -60,24 +64,33 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
data?.start_date && data?.end_date ? getDateRangeStatus(data?.start_date, data?.end_date) : "";
const dateChecker = async (payload: any) => {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
if (isDateGreaterThanToday(payload.end_date)) {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
});
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message: "Unable to create cycle in past date. Please enter a valid date.",
});
}
};
const checkEmptyDate =
@@ -100,7 +113,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<div className="space-y-3">
<div>
<Input
mode="transparent"
autoComplete="off"
id="name"
name="name"
@@ -124,7 +136,6 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register}
/>
@@ -153,7 +164,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({
type: "error",
title: "Error!",
message: "The date you have entered is invalid. Please check and enter a valid date.",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
@@ -184,7 +196,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
setToastAlert({
type: "error",
title: "Error!",
message: "The date you have entered is invalid. Please check and enter a valid date.",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
@@ -207,7 +220,8 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
? "cursor-pointer"
: "cursor-not-allowed"
}
loading={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
disabled={checkEmptyDate ? false : isDateValid ? false : true}
loading={isSubmitting}
>
{status
? isSubmitting

View File

@@ -151,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
@@ -164,7 +164,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
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 transform rounded-lg bg-brand-surface-1 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}

View File

@@ -141,25 +141,25 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center ">
<span
className={`flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-center text-sm capitalize text-brand-muted-1 `}
>
<div className="flex items-center">
<span className="flex items-center rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-center text-xs capitalize">
{capitalizeFirstLetter(cycleStatus)}
</span>
</div>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-brand-muted-1">
<Popover className="flex h-full items-center justify-center rounded-lg">
<div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${
open ? "bg-brand-surface-1" : ""
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
cycle.start_date ? "" : "text-brand-secondary"
}`}
>
<CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span>
<span>
{renderShortDate(new Date(`${cycle?.start_date}`), "Start date")}
</span>
</Popover.Button>
<Transition
@@ -209,20 +209,20 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)}
</Popover>
<span>
<ArrowLongRightIcon className="h-3 w-3" />
<ArrowLongRightIcon className="h-3 w-3 text-brand-secondary" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex items-center gap-1 rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2.5 py-1.5 text-brand-muted-1 ${
open ? "bg-brand-surface-1" : ""
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-brand-base bg-brand-surface-1 px-2 py-1 text-xs ${
cycle.end_date ? "" : "text-brand-secondary"
}`}
>
<CalendarDaysIcon className="h-3 w-3 " />
<CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span>
<span>{renderShortDate(new Date(`${cycle?.end_date}`), "End date")}</span>
</Popover.Button>
<Transition
@@ -234,7 +234,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={
watch("end_date") ? new Date(`${watch("end_date")}`) : new Date()
@@ -275,9 +275,9 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
<div className="flex flex-col gap-6 px-6 py-6 w-full">
<div className="flex flex-col items-start justify-start gap-2 w-full">
<div className="flex items-start justify-between gap-2 w-full">
<div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2">
<h4 className="text-xl font-semibold text-brand-base">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis>
{!isCompleted && (

View File

@@ -238,7 +238,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
return (
<div>
<div className="flex flex-col rounded-[10px] bg-brand-surface-2 text-xs shadow">
<div className="flex flex-col rounded-[10px] bg-brand-base text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
@@ -269,20 +269,20 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
)}
</div>
<div className="flex items-center justify-start gap-5">
<div className="flex items-center justify-start gap-5 text-brand-secondary">
<div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-brand-base" />
<span className="text-brand-secondary">Start :</span>
<CalendarDaysIcon className="h-4 w-4" />
<span>Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<div className="flex items-start gap-1 ">
<TargetIcon className="h-4 w-4 text-brand-base" />
<span className="text-brand-secondary">End :</span>
<TargetIcon className="h-4 w-4" />
<span>End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
<div className="flex items-center justify-between mt-4">
<div className="mt-4 flex items-center justify-between text-brand-secondary">
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
@@ -293,11 +293,11 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
alt={cycle.owned_by.first_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-brand-base capitalize bg-brand-secondary">
<span className="bg-brand-secondary flex h-5 w-5 items-center justify-center rounded-full capitalize">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
<span className="text-brand-base">{cycle.owned_by.first_name}</span>
<span>{cycle.owned_by.first_name}</span>
</div>
<div className="flex items-center">
{!isCompleted && (
@@ -350,7 +350,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col border-t border-brand-base bg-brand-surface-1 ${
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-brand-base bg-brand-surface-2 text-brand-secondary ${
open ? "" : "flex-row"
}`}
>

View File

@@ -106,30 +106,30 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
<Dialog.Panel className="relative transform rounded-lg bg-brand-surface-1 py-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between px-5">
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<TransferIcon className="h-4 w-5" color="#495057" />
<h4 className="text-gray-700 font-medium text-[1.50rem]">Transfer Issues</h4>
<h4 className="text-xl font-medium text-brand-base">Transfer Issues</h4>
</div>
<button onClick={handleClose}>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-2 pb-3 px-5 border-b border-brand-base">
<div className="flex items-center gap-2 border-b border-brand-base px-5 pb-3">
<MagnifyingGlassIcon className="h-4 w-4 text-brand-secondary" />
<input
className="outline-none"
className="bg-brand-surface-1 outline-none"
placeholder="Search for a cycle..."
onChange={(e) => setQuery(e.target.value)}
value={query}
/>
</div>
<div className="flex flex-col items-start w-full gap-2 px-5">
<div className="flex w-full flex-col items-start gap-2 px-5">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option: ICycle) => (
<button
key={option.id}
className="flex items-center gap-4 px-4 py-3 text-gray-600 text-sm rounded w-full hover:bg-brand-surface-1"
className="flex w-full items-center gap-4 rounded px-4 py-3 text-sm text-brand-secondary hover:bg-brand-surface-1"
onClick={() => {
transferIssue({
new_cycle_id: option?.id,
@@ -138,16 +138,16 @@ export const TransferIssuesModal: React.FC<Props> = ({ isOpen, handleClose }) =>
}}
>
<ContrastIcon className="h-5 w-5" />
<div className="flex justify-between w-full">
<div className="flex w-full justify-between">
<span>{option?.name}</span>
<span className=" flex bg-gray-200 capitalize px-2 rounded-full items-center">
<span className=" flex items-center rounded-full bg-brand-surface-2 px-2 capitalize">
{getDateRangeStatus(option?.start_date, option?.end_date)}
</span>
</div>
</button>
))
) : (
<div className="flex items-center justify-center gap-4 p-5 text-sm w-full">
<div className="flex w-full items-center justify-center gap-4 p-5 text-sm">
<ExclamationIcon height={14} width={14} />
<span className="text-center text-brand-secondary">
You dont have any current cycle. Please create one to transfer the

View File

@@ -37,7 +37,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
: 0;
return (
<div className="flex items-center justify-between -mt-4 mb-4">
<div className="-mt-2 mb-4 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-brand-secondary">
<ExclamationIcon height={14} width={14} />
<span>Completed cycles are not editable.</span>
@@ -46,7 +46,7 @@ export const TransferIssues: React.FC<Props> = ({ handleClick }) => {
{transferableIssuesCount > 0 && (
<div>
<PrimaryButton onClick={handleClick} className="flex items-center gap-3 rounded-lg">
<TransferIcon className="h-4 w-4" color="white"/>
<TransferIcon className="h-4 w-4" color="white" />
<span className="text-white">Transfer Issues</span>
</PrimaryButton>
</div>

View File

@@ -36,7 +36,7 @@ const EmojiIconPicker: React.FC<Props> = ({
const [isOpen, setIsOpen] = useState(false);
const [openColorPicker, setOpenColorPicker] = useState(false);
const [activeColor, setActiveColor] = useState<string>("#020617");
const [activeColor, setActiveColor] = useState<string>("#858e96");
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
@@ -69,8 +69,8 @@ const EmojiIconPicker: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] bg-brand-surface-2 shadow-lg">
<div className="h-[230px] w-[250px] overflow-auto border border-brand-base rounded-[4px] bg-brand-surface-2 p-2 shadow-xl">
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-brand-base bg-brand-surface-2 shadow-lg">
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-brand-base bg-brand-surface-2 p-2 shadow-xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => (
@@ -82,7 +82,7 @@ const EmojiIconPicker: React.FC<Props> = ({
setOpenColorPicker(false);
}}
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
selected ? "border-theme text-theme" : "border-transparent text-gray-500"
selected ? "" : "border-transparent text-brand-secondary"
}`}
>
{tab.title}
@@ -95,12 +95,12 @@ const EmojiIconPicker: React.FC<Props> = ({
<Tab.Panel>
{recentEmojis.length > 0 && (
<div className="py-2">
<h3 className="mb-2 ml-1 text-xs text-gray-400">Recent</h3>
<h3 className="mb-2 text-xs text-brand-secondary">Recent</h3>
<div className="grid grid-cols-8 gap-2">
{recentEmojis.map((emoji) => (
<button
type="button"
className="h-4 w-4 select-none text-sm hover:bg-brand-surface-2 flex items-center justify-between"
className="flex h-4 w-4 select-none items-center justify-between text-sm"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -113,13 +113,13 @@ const EmojiIconPicker: React.FC<Props> = ({
</div>
</div>
)}
<hr className="w-full h-[1px] mb-2" />
<hr className="mb-2 h-[1px] w-full border-brand-base" />
<div>
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
{emojis.map((emoji) => (
<button
type="button"
className="h-4 w-4 mb-1 select-none text-sm hover:bg-brand-surface-2 flex items-center"
className="mb-1 flex h-4 w-4 select-none items-center text-sm"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -136,7 +136,7 @@ const EmojiIconPicker: React.FC<Props> = ({
<div className="py-2">
<Tab.Panel className="flex h-full w-full flex-col justify-center">
<div className="relative">
<div className="pb-2 px-1 flex items-center justify-between">
<div className="flex items-center justify-between px-1 pb-2">
{[
"#FF6B00",
"#8CC1FF",
@@ -147,7 +147,7 @@ const EmojiIconPicker: React.FC<Props> = ({
"#000000",
].map((curCol) => (
<span
className="w-4 h-4 rounded-full cursor-pointer"
className="h-4 w-4 cursor-pointer rounded-full"
style={{ backgroundColor: curCol }}
onClick={() => setActiveColor(curCol)}
/>
@@ -158,14 +158,14 @@ const EmojiIconPicker: React.FC<Props> = ({
className="flex items-center gap-1"
>
<span
className="w-4 h-4 rounded-full conical-gradient"
className="conical-gradient h-4 w-4 rounded-full"
style={{ backgroundColor: activeColor }}
/>
</button>
</div>
<div>
<TwitterPicker
className={`m-2 !absolute top-4 left-4 z-10 ${
className={`!absolute top-4 left-4 z-10 m-2 ${
openColorPicker ? "block" : "hidden"
}`}
color={activeColor}
@@ -178,13 +178,12 @@ const EmojiIconPicker: React.FC<Props> = ({
/>
</div>
</div>
<hr className="w-full h-[1px] mb-1" />
<div className="grid grid-cols-8 mt-1 ml-1 gap-x-2 gap-y-3">
<hr className="mb-1 h-[1px] w-full border-brand-base" />
<div className="mt-1 ml-1 grid grid-cols-8 gap-x-2 gap-y-3">
{icons.material_rounded.map((icon) => (
<button
type="button"
className="h-4 w-4 mb-1 select-none text-lg hover:bg-brand-surface-2 flex items-center"
className="mb-1 flex h-4 w-4 select-none items-center text-lg"
key={icon.name}
onClick={() => {
if (onIconsClick) onIconsClick(icon.name);

View File

@@ -184,34 +184,23 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
name: formData.name,
description: formData.description,
},
estimate_points: [
{
key: 0,
value: formData.value1,
},
{
key: 1,
value: formData.value2,
},
{
key: 2,
value: formData.value3,
},
{
key: 3,
value: formData.value4,
},
{
key: 4,
value: formData.value5,
},
{
key: 5,
value: formData.value6,
},
],
estimate_points: [],
};
for (let i = 0; i < 6; i++) {
const point = {
key: i,
value: formData[`value${i + 1}` as keyof FormValues],
};
if (data)
payload.estimate_points.push({
id: data.points[i].id,
...point,
});
else payload.estimate_points.push({ ...point });
}
if (data) await updateEstimate(payload);
else await createEstimate(payload);
};
@@ -244,7 +233,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -258,7 +247,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
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 transform rounded-lg bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-3">
<div className="text-lg font-medium leading-6">

View File

@@ -46,7 +46,7 @@ export const DeleteEstimateModal: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 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">
@@ -60,10 +60,10 @@ export const DeleteEstimateModal: React.FC<Props> = ({
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 transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
@@ -74,9 +74,9 @@ export const DeleteEstimateModal: React.FC<Props> = ({
</span>
</div>
<span>
<p className="break-all text-sm leading-7 text-gray-500">
<p className="break-all text-sm leading-7 text-brand-secondary">
Are you sure you want to delete estimate-{" "}
<span className="break-all font-semibold">{data.name}</span>
<span className="break-all font-medium text-brand-base">{data.name}</span>
{""}? All of the data related to the estiamte will be permanently removed.
This action cannot be undone.
</p>

View File

@@ -0,0 +1,21 @@
import React from "react";
import type { Props } from "./types";
export const CommandIcon: React.FC<Props> = ({
width = "81",
height = "80",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 81 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21.4577 69.8924C18.4684 69.8924 15.922 68.8406 13.8184 66.737C11.7149 64.6335 10.6631 62.087 10.6631 59.0978C10.6631 56.1085 11.7149 53.562 13.8184 51.4585C15.922 49.3549 18.4684 48.3031 21.4577 48.3031H27.2702V31.696H21.4577C18.4684 31.696 15.922 30.6442 13.8184 28.5406C11.7149 26.437 10.6631 23.8906 10.6631 20.9013C10.6631 17.912 11.7149 15.3656 13.8184 13.262C15.922 11.1585 18.4684 10.1067 21.4577 10.1067C24.447 10.1067 26.9934 11.1585 29.097 13.262C31.2006 15.3656 32.2524 17.912 32.2524 20.9013V26.7138H48.8595V20.9013C48.8595 17.912 49.9113 15.3656 52.0149 13.262C54.1184 11.1585 56.6649 10.1067 59.6542 10.1067C62.6434 10.1067 65.1899 11.1585 67.2934 13.262C69.397 15.3656 70.4488 17.912 70.4488 20.9013C70.4488 23.8906 69.397 26.437 67.2934 28.5406C65.1899 30.6442 62.6434 31.696 59.6542 31.696H53.8417V48.3031H59.6542C62.6434 48.3031 65.1899 49.3549 67.2934 51.4585C69.397 53.562 70.4488 56.1085 70.4488 59.0978C70.4488 62.087 69.397 64.6335 67.2934 66.737C65.1899 68.8406 62.6434 69.8924 59.6542 69.8924C56.6649 69.8924 54.1184 68.8406 52.0149 66.737C49.9113 64.6335 48.8595 62.087 48.8595 59.0978V53.2853H32.2524V59.0978C32.2524 62.087 31.2006 64.6335 29.097 66.737C26.9934 68.8406 24.447 69.8924 21.4577 69.8924ZM21.4577 64.9103C23.0631 64.9103 24.4332 64.3428 25.568 63.208C26.7028 62.0732 27.2702 60.7031 27.2702 59.0978V53.2853H21.4577C19.8524 53.2853 18.4823 53.8527 17.3475 54.9875C16.2126 56.1223 15.6452 57.4924 15.6452 59.0978C15.6452 60.7031 16.2126 62.0732 17.3475 63.208C18.4823 64.3428 19.8524 64.9103 21.4577 64.9103ZM59.6542 64.9103C61.2595 64.9103 62.6296 64.3428 63.7644 63.208C64.8992 62.0732 65.4667 60.7031 65.4667 59.0978C65.4667 57.4924 64.8992 56.1223 63.7644 54.9875C62.6296 53.8527 61.2595 53.2853 59.6542 53.2853H53.8417V59.0978C53.8417 60.7031 54.4091 62.0732 55.5439 63.208C56.6787 64.3428 58.0488 64.9103 59.6542 64.9103ZM32.2524 48.3031H48.8595V31.696H32.2524V48.3031ZM21.4577 26.7138H27.2702V20.9013C27.2702 19.296 26.7028 17.9259 25.568 16.7911C24.4332 15.6562 23.0631 15.0888 21.4577 15.0888C19.8524 15.0888 18.4823 15.6562 17.3475 16.7911C16.2126 17.9259 15.6452 19.296 15.6452 20.9013C15.6452 22.5067 16.2126 23.8768 17.3475 25.0116C18.4823 26.1464 19.8524 26.7138 21.4577 26.7138ZM53.8417 26.7138H59.6542C61.2595 26.7138 62.6296 26.1464 63.7644 25.0116C64.8992 23.8768 65.4667 22.5067 65.4667 20.9013C65.4667 19.296 64.8992 17.9259 63.7644 16.7911C62.6296 15.6562 61.2595 15.0888 59.6542 15.0888C58.0488 15.0888 56.6787 15.6562 55.5439 16.7911C54.4091 17.9259 53.8417 19.296 53.8417 20.9013V26.7138Z" />
</svg>
);

View File

@@ -6,29 +6,29 @@ export const CyclesIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "black",
color = "#858E96",
}) => (
<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>
);
<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>
);

View File

@@ -43,7 +43,7 @@ export * from "./user-icon";
export * from "./grid-view-icons";
export * from "./assignment-clipboard-icon";
export * from "./tick-mark-icon";
export * from "./target-icon"
export * from "./target-icon";
export * from "./contrast-icon";
export * from "./people-group-icon";
export * from "./cmd-icon";
@@ -72,4 +72,5 @@ export * from "./svg-file-icon";
export * from "./txt-file-icon";
export * from "./default-file-icon";
export * from "./video-file-icon";
export * from "./audio-file-icon";
export * from "./audio-file-icon";
export * from "./command-icon";

View File

@@ -6,15 +6,21 @@ export const TargetIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "black",
color = "#858E96",
}) => (
<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
width={width}
height={height}
className={className}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="9" cy="9" r="5.4375" stroke={color} strokeLinecap="round" />
<path
fill={color}
stroke-width="0.5"
d="M17.6033 7.51926C18.1597 9.22867 18.1241 11.0757 17.5021 12.7624C16.8802 14.4491 15.7083 15.8771 14.1753 16.8161C12.6424 17.7551 10.8378 18.1504 9.05269 17.9382C7.26757 17.7259 5.60599 16.9185 4.33594 15.6463C3.0659 14.374 2.26145 12.711 2.05235 10.9255C1.84325 9.14002 2.24169 7.33614 3.18341 5.80485C4.12512 4.27355 5.5552 3.10411 7.24298 2.48516C8.93076 1.86621 10.7778 1.83384 12.4863 2.39326L11.4383 3.44026C11.3803 3.49726 11.3283 3.56026 11.2803 3.62526C9.91235 3.34916 8.49189 3.52063 7.22898 4.11431C5.96606 4.70799 4.92774 5.69235 4.26757 6.92182C3.6074 8.15128 3.36045 9.56057 3.56322 10.9413C3.76599 12.3219 4.40773 13.6007 5.39353 14.5884C6.37933 15.5762 7.65683 16.2204 9.03712 16.4259C10.4174 16.6314 11.8272 16.3872 13.0579 15.7295C14.2887 15.0717 15.2751 14.0353 15.8713 12.7736C16.4674 11.5118 16.6417 10.0917 16.3683 8.72326C16.4367 8.67472 16.5009 8.62053 16.5603 8.56126L17.6023 7.51926H17.6033ZM14.8983 9.00026C15.1129 10.0534 14.9826 11.1477 14.5264 12.1209C14.0703 13.0942 13.3127 13.8945 12.3659 14.4033C11.4191 14.9121 10.3336 15.1022 9.27028 14.9456C8.20695 14.7889 7.22239 14.2938 6.46258 13.5336C5.70276 12.7734 5.20814 11.7886 5.05203 10.7252C4.89593 9.66176 5.08665 8.57635 5.59593 7.62984C6.10521 6.68334 6.90593 5.92615 7.87938 5.4705C8.85284 5.01486 9.94721 4.88503 11.0003 5.10026V6.64626C10.2539 6.42346 9.45458 6.45598 8.7288 6.73866C8.00302 7.02135 7.39227 7.53806 6.99325 8.20697C6.59423 8.87589 6.42973 9.65879 6.52581 10.4317C6.62188 11.2047 6.97304 11.9235 7.52368 12.4744C8.07432 13.0252 8.79298 13.3767 9.56588 13.4731C10.3388 13.5695 11.1218 13.4053 11.7908 13.0066C12.4599 12.6079 12.9769 11.9973 13.2599 11.2717C13.5429 10.546 13.5757 9.7467 13.3533 9.00026H14.8983ZM9.99826 11.5003C10.2283 11.5004 10.4553 11.4476 10.6617 11.346C10.868 11.2443 11.0483 11.0966 11.1884 10.9142C11.3286 10.7318 11.4249 10.5196 11.47 10.294C11.515 10.0684 11.5076 9.83551 11.4483 9.61326L13.0303 8.03026L13.0603 8.00026H15.5003C15.566 8.00038 15.631 7.98754 15.6918 7.96249C15.7525 7.93744 15.8077 7.90066 15.8543 7.85426L17.8543 5.85426C17.9244 5.78433 17.9721 5.69516 17.9915 5.59805C18.0109 5.50094 18.001 5.40027 17.963 5.3088C17.9251 5.21732 17.8609 5.13917 17.7785 5.08424C17.6961 5.02931 17.5993 5.00008 17.5003 5.00026H15.0003V2.50026C15.0002 2.40144 14.9709 2.30485 14.9161 2.22268C14.8612 2.1405 14.7832 2.07643 14.6919 2.03855C14.6006 2.00068 14.5002 1.99069 14.4033 2.00986C14.3063 2.02903 14.2172 2.0765 14.1473 2.14626L12.1473 4.14626C12.1007 4.1927 12.0637 4.24787 12.0385 4.30861C12.0133 4.36936 12.0003 4.43448 12.0003 4.50026V6.94026C11.99 6.94998 11.98 6.95998 11.9703 6.97026L10.3883 8.55026C10.1658 8.49057 9.93264 8.48287 9.70675 8.52775C9.48086 8.57263 9.26833 8.66889 9.0856 8.80908C8.90288 8.94926 8.75486 9.12961 8.65302 9.33617C8.55117 9.54273 8.49821 9.76996 8.49826 10.0003C8.49826 10.3981 8.65629 10.7796 8.9376 11.0609C9.2189 11.3422 9.60043 11.5003 9.99826 11.5003Z"
/>
</svg>
);
);

View File

@@ -73,7 +73,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -87,12 +87,12 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
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 transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
className="h-6 w-6 text-red-500"
aria-hidden="true"
/>
</span>
@@ -101,16 +101,19 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
</span>
</div>
<span>
<p className="text-sm leading-7 text-gray-500">
Are you sure you want to delete project{" "}
<span className="break-all font-semibold">{data?.service}</span>? All of the
data related to the project will be permanently removed. This action cannot be
undone
<p className="text-sm leading-7 text-brand-secondary">
Are you sure you want to delete import from{" "}
<span className="break-all font-semibold capitalize text-brand-base">
{data?.service}
</span>
? All of the data related to the import will be permanently removed. This
action cannot be undone.
</p>
</span>
<div className="text-gray-600">
<p className="text-sm">
To confirm, type <span className="font-medium">delete import</span> below:
<div>
<p className="text-sm text-brand-secondary">
To confirm, type{" "}
<span className="font-medium text-brand-base">delete import</span> below:
</p>
<Input
type="text"

View File

@@ -36,7 +36,7 @@ export const GithubImportConfigure: React.FC<Props> = ({
<div className="flex items-center gap-2 py-5">
<div className="w-full">
<div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your GitHub import.</div>
<div className="text-sm text-brand-secondary">Set up your GitHub import.</div>
</div>
<div className="flex-shrink-0">
<GithubAuth workspaceIntegration={workspaceIntegration} provider={provider} />

View File

@@ -14,7 +14,7 @@ type Props = {
export const GithubImportConfirm: FC<Props> = ({ handleStepChange, watch }) => (
<div className="mt-6">
<h4 className="font-medium">
<h4 className="font-medium text-brand-secondary">
You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm &
Import{'" '}
to complete the process.

View File

@@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC } from "react";
// react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form";
@@ -7,7 +7,7 @@ import useProjects from "hooks/use-projects";
// components
import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration";
// ui
import { CustomSearchSelect, PrimaryButton, SecondaryButton } from "components/ui";
import { CustomSearchSelect, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
@@ -36,7 +36,7 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Repository</h4>
<p className="text-gray-500 text-xs">
<p className="text-xs text-brand-secondary">
Select the repository that you want the issues to be imported from.
</p>
</div>
@@ -49,7 +49,13 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<SelectRepository
integration={integration}
value={value ? value.id : null}
label={value ? `${value.full_name}` : "Select Repository"}
label={
value ? (
`${value.full_name}`
) : (
<span className="text-brand-secondary">Select Repository</span>
)
}
onChange={onChange}
characterLimit={50}
/>
@@ -61,7 +67,9 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Project</h4>
<p className="text-gray-500 text-xs">Select the project to import the issues to.</p>
<p className="text-xs text-brand-secondary">
Select the project to import the issues to.
</p>
</div>
<div className="col-span-12 sm:col-span-4">
{projects && (
@@ -71,7 +79,13 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? projects.find((p) => p.id === value)?.name : "Select Project"}
label={
value ? (
projects.find((p) => p.id === value)?.name
) : (
<span className="text-brand-secondary">Select Project</span>
)
}
onChange={onChange}
options={options}
optionsClassName="w-full"
@@ -84,30 +98,16 @@ export const GithubImportData: FC<Props> = ({ handleStepChange, integration, con
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Sync Issues</h4>
<p className="text-gray-500 text-xs">Set whether you want to sync the issues or not.</p>
<p className="text-xs text-brand-secondary">
Set whether you want to sync the issues or not.
</p>
</div>
<div className="col-span-12 sm:col-span-4">
<Controller
control={control}
name="sync"
render={({ field: { value, onChange } }) => (
<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 ${
value ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={value ? true : false}
onClick={() => onChange(!value)}
>
<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-white shadow ring-0 transition duration-200 ease-in-out ${
value ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
<ToggleSwitch value={value} onChange={() => onChange(!value)} />
)}
/>
</div>

View File

@@ -25,9 +25,9 @@ export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers
return (
<div className="mt-6">
<div>
<div className="grid grid-cols-3 gap-2 text-sm mb-2 font-medium">
<div>Name</div>
<div>Import as...</div>
<div className="mb-2 grid grid-cols-3 gap-2 text-sm font-medium">
<div className="text-brand-secondary">Name</div>
<div className="text-brand-secondary">Import as...</div>
<div className="text-right">
{users.filter((u) => u.import !== false).length} users selected
</div>

View File

@@ -64,20 +64,20 @@ export const GithubRepoDetails: FC<Props> = ({
<div className="flex items-center justify-between gap-4">
<div>
<div className="font-medium">Repository Details</div>
<div className="text-sm text-gray-600">Import completed. We have found:</div>
<div className="text-sm text-brand-secondary">Import completed. We have found:</div>
</div>
<div className="flex gap-16 mt-4">
<div className="text-center flex-shrink-0">
<div className="mt-4 flex gap-16">
<div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.issue_count}</p>
<h6 className="text-sm text-gray-500">Issues</h6>
<h6 className="text-sm text-brand-secondary">Issues</h6>
</div>
<div className="text-center flex-shrink-0">
<div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.labels}</p>
<h6 className="text-sm text-gray-500">Labels</h6>
<h6 className="text-sm text-brand-secondary">Labels</h6>
</div>
<div className="text-center flex-shrink-0">
<div className="flex-shrink-0 text-center">
<p className="text-3xl font-bold">{repoInfo.collaborators.length}</p>
<h6 className="text-sm text-gray-500">Users</h6>
<h6 className="text-sm text-brand-secondary">Users</h6>
</div>
</div>
</div>

View File

@@ -175,15 +175,13 @@ export const GithubImporterRoot = () => {
<form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
<div>
<ArrowLeftIcon className="h-3 w-3" />
</div>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-brand-secondary hover:text-brand-base">
<ArrowLeftIcon className="h-3 w-3" />
<div>Cancel import & go back</div>
</div>
</Link>
<div className="space-y-4 rounded-[10px] border border-gray-200 bg-white p-4">
<div className="space-y-4 rounded-[10px] border border-brand-base bg-brand-base p-4">
<div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0">
<Image src={GithubLogo} alt="GithubLogo" />
@@ -194,12 +192,12 @@ export const GithubImporterRoot = () => {
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
? `border-brand-accent bg-brand-accent ${
index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80"
}`
: "border-gray-300"
: "border-brand-base"
}`}
>
<integration.icon
@@ -213,8 +211,8 @@ export const GithubImporterRoot = () => {
key={index}
className={`border-b px-7 ${
index <= activeIntegrationState() - 1
? `border-[#3F76FF]`
: `border-gray-300`
? `border-brand-accent`
: `border-brand-base`
}`}
>
{" "}

View File

@@ -11,12 +11,12 @@ import { CustomSearchSelect } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IWorkspaceIntegration } from "types";
import { IWorkspaceIntegration, IGithubRepository } from "types";
type Props = {
integration: IWorkspaceIntegration;
value: any;
label: string;
label: string | JSX.Element;
onChange: (repo: any) => void;
characterLimit?: number;
};
@@ -54,7 +54,9 @@ export const SelectRepository: React.FC<Props> = ({
isValidating,
} = useSWRInfinite(getKey, fetchGithubRepos);
const userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
let userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
userRepositories = userRepositories.filter((data) => data?.id);
const totalCount = paginatedData && paginatedData.length > 0 ? paginatedData[0].total_count : 0;
const options =

View File

@@ -64,9 +64,9 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
})) ?? [];
return (
<div className="bg-gray-50 px-2 py-3 rounded-md grid grid-cols-3 items-center gap-2">
<div className="grid grid-cols-3 items-center gap-2 rounded-md bg-brand-surface-2 px-2 py-3">
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 rounded flex-shrink-0">
<div className="relative h-8 w-8 flex-shrink-0 rounded">
<Image
src={collaborator.avatar_url}
layout="fill"
@@ -112,7 +112,7 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
setUsers(newUsers);
}}
placeholder="Enter email of the user"
className="py-1 border-gray-200 text-xs"
className="py-1 text-xs"
/>
)}
{users[index].import === "map" && members && (

View File

@@ -52,10 +52,10 @@ const IntegrationGuide = () => {
handleClose={() => setDeleteImportModal(false)}
data={importToDelete}
/>
<div className="space-y-2 h-full">
<div className="h-full space-y-2">
{!provider && (
<>
<div className="flex items-center gap-2 mb-5">
<div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm">
@@ -72,7 +72,10 @@ const IntegrationGuide = () => {
</div>
<div className="space-y-2">
{IMPORTERS_EXPORTERS_LIST.map((service) => (
<div key={service.provider} className="rounded-[10px] border bg-white p-4">
<div
key={service.provider}
className="rounded-[10px] border border-brand-base bg-brand-base p-4"
>
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
@@ -84,7 +87,7 @@ const IntegrationGuide = () => {
</div>
<div className="w-full">
<h3>{service.title}</h3>
<p className="text-sm text-gray-500">{service.description}</p>
<p className="text-sm text-brand-secondary">{service.description}</p>
</div>
<div className="flex-shrink-0">
<Link
@@ -101,12 +104,12 @@ const IntegrationGuide = () => {
</div>
))}
</div>
<div className="rounded-[10px] border bg-white p-4">
<h3 className="mb-2 font-medium text-lg flex gap-2">
<div className="rounded-[10px] border border-brand-base bg-brand-base p-4">
<h3 className="mb-2 flex gap-2 text-lg font-medium">
Previous Imports
<button
type="button"
className="flex-shrink-0 flex items-center gap-1 outline-none text-xs py-1 px-1.5 bg-gray-100 rounded"
className="flex flex-shrink-0 items-center gap-1 rounded bg-brand-surface-2 py-1 px-1.5 text-xs outline-none"
onClick={() => {
setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
@@ -133,10 +136,12 @@ const IntegrationGuide = () => {
</div>
</div>
) : (
<div className="py-2 text-sm text-gray-800">No previous imports available.</div>
<p className="py-2 text-sm text-brand-secondary">
No previous imports available.
</p>
)
) : (
<Loader className="grid grid-cols-1 gap-3 mt-6">
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />

View File

@@ -8,3 +8,5 @@ export * from "./single-integration-card";
export * from "./github";
// jira
export * from "./jira";
// slack
export * from "./slack";

View File

@@ -17,30 +17,30 @@ export const JiraConfirmImport: React.FC = () => {
</div>
<div className="col-span-1">
<p className="text-sm text-gray-500">Migrating</p>
<p className="text-sm text-brand-secondary">Migrating</p>
</div>
<div className="col-span-1 flex items-center justify-between">
<div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_issues")}</h4>
<p className="text-sm text-gray-500">Issues</p>
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_issues")}</h4>
<p className="text-sm text-brand-secondary">Issues</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_states")}</h4>
<p className="text-sm text-gray-500">States</p>
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_states")}</h4>
<p className="text-sm text-brand-secondary">States</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_modules")}</h4>
<p className="text-sm text-gray-500">Modules</p>
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_modules")}</h4>
<p className="text-sm text-brand-secondary">Modules</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{watch("data.total_labels")}</h4>
<p className="text-sm text-gray-500">Labels</p>
<h4 className="mb-2 text-lg font-semibold">{watch("data.total_labels")}</h4>
<p className="text-sm text-brand-secondary">Labels</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">
<h4 className="mb-2 text-lg font-semibold">
{watch("data.users").filter((user) => user.import).length}
</h4>
<p className="text-sm text-gray-500">User</p>
<p className="text-sm text-brand-secondary">User</p>
</div>
</div>
</div>

View File

@@ -30,15 +30,11 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="h-full w-full space-y-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Personal Access Token</h3>
<p className="text-sm text-gray-500">
<h3 className="font-semibold">Jira Personal Access Token</h3>
<p className="text-sm text-brand-secondary">
Get to know your access token by navigating to{" "}
<Link href="https://id.atlassian.com/manage-profile/security/api-tokens">
<a
className="font-medium text-gray-600 hover:text-gray-900"
target="_blank"
rel="noreferrer"
>
<a className="text-brand-accent underline" target="_blank" rel="noreferrer">
Atlassian Settings
</a>
</Link>
@@ -61,8 +57,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Project Key</h3>
<p className="text-sm text-gray-500">If XXX-123 is your issue, then enter XXX</p>
<h3 className="font-semibold">Jira Project Key</h3>
<p className="text-sm text-brand-secondary">If XXX-123 is your issue, then enter XXX</p>
</div>
<div className="col-span-1">
<Input
@@ -80,8 +76,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Email Address</h3>
<p className="text-sm text-gray-500">
<h3 className="font-semibold">Jira Email Address</h3>
<p className="text-sm text-brand-secondary">
Enter the Gmail account that you use in Jira account
</p>
</div>
@@ -102,8 +98,8 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Jira Installation or Cloud Host Name</h3>
<p className="text-sm text-gray-500">Enter your companies cloud host name</p>
<h3 className="font-semibold">Jira Installation or Cloud Host Name</h3>
<p className="text-sm text-brand-secondary">Enter your companies cloud host name</p>
</div>
<div className="col-span-1">
<Input
@@ -122,8 +118,10 @@ export const JiraGetImportDetail: React.FC = () => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Import to project</h3>
<p className="text-sm text-gray-500">Select which project you want to import to.</p>
<h3 className="font-semibold">Import to project</h3>
<p className="text-sm text-brand-secondary">
Select which project you want to import to.
</p>
</div>
<div className="col-span-1">
<Controller
@@ -138,9 +136,11 @@ export const JiraGetImportDetail: React.FC = () => {
onChange={onChange}
label={
<span>
{value && value !== ""
? projects.find((p) => p.id === value)?.name
: "Select Project"}
{value && value !== "" ? (
projects.find((p) => p.id === value)?.name
) : (
<span className="text-brand-secondary">Select a project</span>
)}
</span>
}
>
@@ -151,7 +151,7 @@ export const JiraGetImportDetail: React.FC = () => {
</CustomSelect.Option>
))
) : (
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500">
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-brand-secondary">
<p>You don{"'"}t have any project. Please create a project first.</p>
</div>
)}
@@ -162,7 +162,7 @@ export const JiraGetImportDetail: React.FC = () => {
const event = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(event);
}}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-gray-500"
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-brand-secondary"
>
<PlusIcon className="h-4 w-4 text-gray-500" />
<span>Create new project</span>

View File

@@ -52,11 +52,13 @@ export const JiraImportUsers: FC = () => {
})) ?? [];
return (
<div className="h-full w-full space-y-10 divide-y-2 overflow-y-auto">
<div className="h-full w-full space-y-10 divide-y-2 divide-brand-base overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Users</h3>
<p className="text-sm text-gray-500">Update, invite or choose not to invite assignee</p>
<h3 className="font-semibold">Users</h3>
<p className="text-sm text-brand-secondary">
Update, invite or choose not to invite assignee
</p>
</div>
<div className="col-span-1">
<Controller
@@ -72,8 +74,8 @@ export const JiraImportUsers: FC = () => {
{watch("data.invite_users") && (
<div className="pt-6">
<div className="grid grid-cols-3 gap-3">
<div className="col-span-1 text-gray-500">Name</div>
<div className="col-span-1 text-gray-500">Import as</div>
<div className="col-span-1 text-sm text-brand-secondary">Name</div>
<div className="col-span-1 text-sm text-brand-secondary">Import as</div>
</div>
<div className="mt-5 space-y-3">

View File

@@ -102,12 +102,12 @@ export const JiraProjectDetail: React.FC<Props> = (props) => {
if (error) {
return (
<div className="flex h-full w-full items-center justify-center">
<p className="text-sm text-gray-500">
<p className="text-sm text-brand-secondary">
Something went wrong. Please{" "}
<button
onClick={() => setCurrentStep({ state: "import-configure" })}
type="button"
className="inline text-blue-500 underline"
className="inline text-brand-accent underline"
>
go back
</button>{" "}
@@ -121,37 +121,37 @@ export const JiraProjectDetail: React.FC<Props> = (props) => {
<div className="h-full w-full space-y-10 overflow-y-auto">
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Import Data</h3>
<p className="text-sm text-gray-500">Import Completed. We have found:</p>
<h3 className="font-semibold">Import Data</h3>
<p className="text-sm text-brand-secondary">Import Completed. We have found:</p>
</div>
<div className="col-span-1 flex items-center justify-between">
<div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.issues}</h4>
<p className="text-sm text-gray-500">Issues</p>
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.issues}</h4>
<p className="text-sm text-brand-secondary">Issues</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.states}</h4>
<p className="text-sm text-gray-500">States</p>
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.states}</h4>
<p className="text-sm text-brand-secondary">States</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.modules}</h4>
<p className="text-sm text-gray-500">Modules</p>
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.modules}</h4>
<p className="text-sm text-brand-secondary">Modules</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.labels}</h4>
<p className="text-sm text-gray-500">Labels</p>
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.labels}</h4>
<p className="text-sm text-brand-secondary">Labels</p>
</div>
<div>
<h4 className="mb-2 text-xl font-semibold">{projectInfo?.users?.length}</h4>
<p className="text-sm text-gray-500">Users</p>
<h4 className="mb-2 text-lg font-semibold">{projectInfo?.users?.length}</h4>
<p className="text-sm text-brand-secondary">Users</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1">
<h3 className="text-lg font-semibold">Import Epics</h3>
<p className="text-sm text-gray-500">Import epics as modules</p>
<h3 className="font-semibold">Import Epics</h3>
<p className="text-sm text-brand-secondary">Import epics as modules</p>
</div>
<div className="col-span-1">
<Controller

View File

@@ -106,7 +106,7 @@ export const JiraImporterRoot = () => {
return (
<div className="flex h-full flex-col space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-brand-secondary hover:text-brand-base">
<div>
<ArrowLeftIcon className="h-3 w-3" />
</div>
@@ -114,7 +114,7 @@ export const JiraImporterRoot = () => {
</div>
</Link>
<div className="flex h-full flex-col space-y-4 rounded-[10px] border border-gray-200 bg-white p-4">
<div className="flex h-full flex-col space-y-4 rounded-[10px] border border-brand-base bg-brand-base p-4">
<div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0">
<Image src={JiraLogo} alt="jira logo" />
@@ -131,14 +131,14 @@ export const JiraImporterRoot = () => {
index > activeIntegrationState() + 1 ||
Boolean(index === activeIntegrationState() + 1 && disableTopBarAfter)
}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border border-brand-base ${
index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
? `border-brand-accent bg-brand-accent ${
index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80"
}`
: "border-gray-300"
: "border-brand-base"
}`}
>
<integration.icon
@@ -151,7 +151,9 @@ export const JiraImporterRoot = () => {
<div
key={index}
className={`border-b px-7 ${
index <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
index <= activeIntegrationState() - 1
? `border-brand-accent`
: `border-brand-base`
}`}
>
{" "}
@@ -177,7 +179,7 @@ export const JiraImporterRoot = () => {
{currentStep?.state === "import-confirmation" && <JiraConfirmImport />}
</div>
<div className="-mx-4 mt-4 flex justify-end gap-4 border-t p-4 pb-0">
<div className="-mx-4 mt-4 flex justify-end gap-4 border-t border-brand-base p-4 pb-0">
{currentStep?.state !== "import-configure" && (
<SecondaryButton
onClick={() => {

View File

@@ -18,28 +18,28 @@ const importersList: { [key: string]: string } = {
};
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="py-3 flex justify-between items-center gap-2">
<div className="flex items-center justify-between gap-2 py-3">
<div>
<h4 className="text-sm flex items-center gap-2">
<h4 className="flex items-center gap-2 text-sm">
<span>
Import from <span className="font-medium">{importersList[service.service]}</span> to{" "}
<span className="font-medium">{service.project_detail.name}</span>
</span>
<span
className={`capitalize px-2 py-0.5 text-xs rounded ${
className={`rounded px-2 py-0.5 text-xs capitalize ${
service.status === "completed"
? "bg-green-100 text-green-500"
? "bg-green-500/20 text-green-500"
: service.status === "processing"
? "bg-yellow-100 text-yellow-500"
? "bg-yellow-500/20 text-yellow-500"
: service.status === "failed"
? "bg-red-100 text-red-500"
? "bg-red-500/20 text-red-500"
: ""
}`}
>
{refreshing ? "Refreshing..." : service.status}
</span>
</h4>
<div className="text-gray-500 text-xs mt-2 flex items-center gap-2">
<div className="mt-2 flex items-center gap-2 text-xs text-brand-secondary">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span>
Imported by{" "}

View File

@@ -11,7 +11,7 @@ import IntegrationService from "services/integration";
import useToast from "hooks/use-toast";
import useIntegrationPopup from "hooks/use-integration-popup";
// ui
import { DangerButton, Loader, SecondaryButton } from "components/ui";
import { DangerButton, Loader, PrimaryButton } from "components/ui";
// icons
import GithubLogo from "public/services/github.png";
import SlackLogo from "public/services/slack.png";
@@ -33,7 +33,7 @@ const integrationDetails: { [key: string]: any } = {
},
slack: {
logo: SlackLogo,
installed: "Activate Slack integrations on individual projects to sync with specific cahnnels.",
installed: "Activate Slack integrations on individual projects to sync with specific channels.",
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
},
};
@@ -99,7 +99,7 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
);
return (
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-brand-base bg-brand-surface-2 p-5">
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-brand-base bg-brand-base p-5">
<div className="flex items-start gap-4">
<div className="h-12 w-12 flex-shrink-0">
<Image
@@ -139,9 +139,9 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
{deletingIntegration ? "Removing..." : "Remove installation"}
</DangerButton>
) : (
<SecondaryButton onClick={startAuth} loading={isInstalling}>
<PrimaryButton onClick={startAuth} loading={isInstalling}>
{isInstalling ? "Installing..." : "Add installation"}
</SecondaryButton>
</PrimaryButton>
)
) : (
<Loader>

View File

@@ -0,0 +1 @@
export * from "./select-channel";

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// services
import appinstallationsService from "services/app-installations.service";
// ui
import { Loader } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
import useIntegrationPopup from "hooks/use-integration-popup";
// types
import { IWorkspaceIntegration, ISlackIntegration } from "types";
// fetch-keys
import { SLACK_CHANNEL_INFO } from "constants/fetch-keys";
type Props = {
integration: IWorkspaceIntegration;
};
export const SelectChannel: React.FC<Props> = ({ integration }) => {
const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] =
useState<boolean>(false);
const [slackChannel, setSlackChannel] = useState<ISlackIntegration | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { startAuth } = useIntegrationPopup("slackChannel", integration.id);
const { data: projectIntegration } = useSWR(
workspaceSlug && projectId && integration.id
? SLACK_CHANNEL_INFO(workspaceSlug as string, projectId as string)
: null,
() =>
workspaceSlug && projectId && integration.id
? appinstallationsService.getSlackChannelDetail(
workspaceSlug as string,
projectId as string,
integration.id as string
)
: null
);
useEffect(() => {
if (projectId && projectIntegration && projectIntegration.length > 0) {
const projectSlackIntegrationCheck: ISlackIntegration | undefined = projectIntegration.find(
(_slack: ISlackIntegration) => _slack.project === projectId
);
if (projectSlackIntegrationCheck) {
setSlackChannel(() => projectSlackIntegrationCheck);
setSlackChannelAvailabilityToggle(true);
}
}
}, [projectIntegration, projectId]);
const handleDelete = async () => {
if (projectIntegration.length === 0) return;
mutate(SLACK_CHANNEL_INFO, (prevData: any) => {
if (!prevData) return;
return prevData.id !== integration.id;
}).then(() => {
setSlackChannelAvailabilityToggle(false);
setSlackChannel(null);
});
appinstallationsService
.removeSlackChannel(
workspaceSlug as string,
projectId as string,
integration.id as string,
slackChannel?.id
)
.catch((err) => console.log(err));
};
const handleAuth = async () => {
await startAuth();
};
return (
<>
{projectIntegration ? (
<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 ${
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked
onClick={() => {
slackChannelAvailabilityToggle ? handleDelete() : handleAuth();
}}
>
<span
aria-hidden="true"
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0"
}`}
/>
</button>
) : (
<Loader>
<Loader.Item height="35px" width="150px" />
</Loader>
)}
</>
);
};

View File

@@ -110,9 +110,7 @@ const activityDetails: {
},
};
type Props = {};
export const IssueActivitySection: React.FC<Props> = () => {
export const IssueActivitySection: React.FC = () => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;

View File

@@ -34,7 +34,7 @@ export const IssueAttachments = () => {
workspaceSlug && projectId && issueId ? ISSUE_ATTACHMENTS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.getIssueAttachment(
issuesService.getIssueAttachment(
workspaceSlug as string,
projectId as string,
issueId as string
@@ -61,7 +61,7 @@ export const IssueAttachments = () => {
attachments.map((file) => (
<div
key={file.id}
className="flex items-center justify-between h-[60px] gap-1 px-4 py-2 text-sm border border-gray-200 bg-white rounded-md"
className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-brand-surface-2 bg-brand-base px-4 py-2 text-sm"
>
<Link href={file.asset}>
<a target="_blank">
@@ -87,7 +87,7 @@ export const IssueAttachments = () => {
</Tooltip>
</div>
<div className="flex items-center gap-3 text-gray-500 text-xs">
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{getFileExtension(file.asset).toUpperCase()}</span>
<span>{convertBytesToSize(file.attributes.size)}</span>
</div>

View File

@@ -68,7 +68,10 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
)}
<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" aria-hidden="true" />
<ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-brand-secondary"
aria-hidden="true"
/>
</span>
</div>
<div className="min-w-0 flex-1">
@@ -77,7 +80,9 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
{comment.actor_detail.first_name}
{comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name}
</div>
<p className="mt-0.5 text-xs text-brand-secondary">Commented {timeAgo(comment.created_at)}</p>
<p className="mt-0.5 text-xs text-brand-secondary">
Commented {timeAgo(comment.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
{isEditing ? (
@@ -94,13 +99,13 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
<button
type="submit"
disabled={isSubmitting}
className="group rounded border border-green-500 bg-green-100 p-2 shadow-md duration-300 hover:bg-green-500"
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
>
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
</button>
<button
type="button"
className="group rounded border border-red-500 bg-red-100 p-2 shadow-md duration-300 hover:bg-red-500"
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
onClick={() => setIsEditing(false)}
>
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
@@ -108,16 +113,11 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
</div>
</form>
) : (
// <div
// className="mt-2 mb-6 text-sm text-gray-700"
// dangerouslySetInnerHTML={{ __html: comment.comment_html }}
// />
<RemirrorRichTextEditor
value={comment.comment_html}
editable={false}
onBlur={() => ({})}
noBorder
customClassName="text-xs bg-brand-surface-1"
customClassName="text-xs border border-brand-base bg-brand-base"
/>
)}
</div>

View File

@@ -75,7 +75,7 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@@ -89,8 +89,8 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
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 transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="bg-brand-surface-2 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon
@@ -101,12 +101,12 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
className="text-lg font-medium leading-6 text-brand-base"
>
Delete Attachment
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
<p className="text-sm text-brand-secondary">
Are you sure you want to delete attachment-{" "}
<span className="font-bold">{getFileName(data.attributes.name)}</span>?
This attachment will be permanently removed. This action cannot be
@@ -116,7 +116,7 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
</div>
</div>
</div>
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
<div className="flex justify-end gap-2 bg-brand-surface-1 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton
onClick={() => {

View File

@@ -88,7 +88,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 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">
@@ -102,10 +102,10 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
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 transform overflow-hidden rounded-lg bg-brand-surface-2 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-brand-base bg-brand-base text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-100 p-4">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
@@ -116,9 +116,9 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data })
</span>
</div>
<span>
<p className="break-all text-sm leading-7 text-brand-secondary">
<p className="text-sm text-brand-secondary">
Are you sure you want to delete issue{" "}
<span className="break-all font-semibold">
<span className="break-all font-medium text-brand-base">
{data?.project_detail.identifier}-{data?.sequence_id}
</span>
{""}? All of the data related to the issue will be permanently removed. This

View File

@@ -26,7 +26,14 @@ import { CreateStateModal } from "components/states";
import { CreateUpdateCycleModal } from "components/cycles";
import { CreateLabelModal } from "components/labels";
// ui
import { CustomMenu, Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
import {
CustomMenu,
Input,
Loader,
PrimaryButton,
SecondaryButton,
ToggleSwitch,
} from "components/ui";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers
@@ -221,7 +228,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</h3>
</div>
{watch("parent") && watch("parent") !== "" ? (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-brand-surface-1 p-2 text-xs">
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-brand-surface-2 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
@@ -230,7 +237,7 @@ export const IssueForm: FC<IssueFormProps> = ({
.color,
}}
/>
<span className="flex-shrink-0 text-gray-600">
<span className="flex-shrink-0 text-brand-secondary">
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
{issues.find((i) => i.id === watch("parent"))?.sequence_id}
</span>
@@ -253,7 +260,6 @@ export const IssueForm: FC<IssueFormProps> = ({
onChange={handleTitleChange}
className="resize-none text-xl"
placeholder="Title"
mode="transparent"
autoComplete="off"
error={errors.name}
register={register}
@@ -294,7 +300,7 @@ export const IssueForm: FC<IssueFormProps> = ({
)}
</div>
<div className="relative">
<div className="flex justify-end -mb-2">
<div className="-mb-2 flex justify-end">
{issueName && issueName !== "" && (
<button
type="button"
@@ -450,22 +456,7 @@ export const IssueForm: FC<IssueFormProps> = ({
onClick={() => setCreateMore((prevData) => !prevData)}
>
<span className="text-xs">Create more</span>
<button
type="button"
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
createMore ? "bg-brand-accent" : "bg-gray-300"
} transition-colors duration-300 ease-in-out focus:outline-none`}
role="switch"
aria-checked="false"
>
<span className="sr-only">Create more</span>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-3 w-3 ${
createMore ? "translate-x-3" : "translate-x-0"
} transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-300 ease-in-out`}
/>
</button>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>

View File

@@ -219,11 +219,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-[#131313] bg-opacity-50 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">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@@ -233,7 +233,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
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 transform rounded-lg bg-brand-surface-1 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform rounded-lg border border-brand-base bg-brand-base p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
issues={issues ?? []}
handleFormSubmit={handleFormSubmit}

Some files were not shown because too many files have changed in this diff Show More