Compare commits

...
Author SHA1 Message Date
Aaryan Khandelwal aba8b23d92 fix: merge conflicts resolved from develop 2024-03-06 19:45:50 +05:30
guru_sainathandGitHub dd579f83ee chore: enabled module and cycle display properties in module and cycle issues (#3885) 2024-03-06 19:27:04 +05:30
66f2492e60 [WEB-655] chore: peek overview dropdowns keyboard navigation improvement (#3888)
* fix: enums export in the types package

* chore: remove NestedKeyOf type

* chore: peek overview keyboard accessibility improvement

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-03-06 19:17:50 +05:30
Aaryan KhandelwalandGitHub c08d6987d0 [WEB-658] fix: multiple toast alerts on adding existing issues (#3889)
* fix: enums export in the types package

* chore: remove NestedKeyOf type

* fix: multiple toast alerts on adding existing issues

* chore: added success toast alerts
2024-03-06 19:17:00 +05:30
e4f48d6878 [WEB-393] feat: new emoji picker using emoji-picker-react (#3868)
* chore: emoji-picker-react package added

* chore: emoji and emoji picker component added

* chore: emoji picker custom style added

* chore: migration of the emoji's

* chore: migration changes

* chore: project logo prop

* chore: added logo props in the serializer

* chore: removed unused keys

* chore: implement emoji picker throughout the web app

* style: emoji icon picker

* chore: update project logo renderer in the space app

* chore: migrations fixes

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-03-06 19:15:48 +05:30
Aaryan KhandelwalandGitHub b3d3c0fb06 [WEB-654] fix: enums export in the types package (#3887)
* fix: enums export in the types package

* chore: remove NestedKeyOf type
2024-03-06 18:46:36 +05:30
M. PalanikannanandGitHub cace132a2a [WEB-372] fix: horizontal rule extension now always divider adds below nodes (#3890)
* fix: horizontal rule extension now always divider adds below nodes

* chore: removing duplicate horizontal rule extension
2024-03-06 18:43:19 +05:30
3d09a69d58 fix: eslint issues and reconfiguring (#3891)
* fix: eslint fixes

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-06 18:39:14 +05:30
sriram veeraghanta 921b9078f1 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-06 14:33:15 +05:30
M. PalanikannanandGitHub e1db39ffc8 [WEB-566] feat: Added text to empty color picker boxes and other fixes (#3867)
* fix: z index issues with modals and on hover color in table item picker menu

* feat: added text indicators inside the table colors to give a gist of how text would look
2024-03-06 14:29:52 +05:30
Anmol Singh BhatiaandGitHub 2b05d23470 fix: kanban card state overflow fix (#3866) 2024-03-06 14:28:24 +05:30
Anmol Singh BhatiaandGitHub 69fa1708cc fix: module quick action dropdown overflow fix (#3865) 2024-03-06 14:27:59 +05:30
Aaryan KhandelwalandGitHub 666b7ea577 fix: remove member button alignment (#3862) 2024-03-06 14:27:41 +05:30
Anmol Singh BhatiaandGitHub 7b76df6868 [WEB-581] chore: project states setting page improvement (#3864)
* chore: project states setting page improvement

* chore: code refactor

* chore: observer added in project state setting page
2024-03-06 14:26:34 +05:30
dbdd14493b [WEB-662] chore: posthog debugging based on env variable (#3860)
* chore: posthog debugging based on env variable

* updated turbo.json

* chore: true to 1

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-03-06 14:25:15 +05:30
Bavisetti NarayanandGitHub 4572b7378d [WEB-621] chore: 404 page not found (#3859)
* chore: custom 404 error

* chore: moved from middleware to view
2024-03-06 14:24:58 +05:30
5a32d10f96 [WEB-373] chore: new dashboard updates (#3849)
* chore: replaced marimekko graph with a bar graph

* chore: add bar onClick handler

* chore: custom date filter for widgets

* style: priority graph

* chore: workspace profile activity pagination

* chore: profile activity pagination

* chore: user profile activity pagination

* chore: workspace user activity csv download

* chore: download activity button added

* chore: workspace user pagination

* chore: collabrator pagination

* chore: field change

* chore: recent collaborators pagination

* chore: changed the collabrators

* chore: collabrators list changed

* fix: distinct users

* chore: search filter in collaborators

* fix: import error

* chore: update priority graph x-axis values

* chore: admin and member request validation

* chore: update csv download request method

* chore: search implementation for the collaborators widget

* refactor: priority distribution card

* chore: add enum for duration filters

* chore: update inbox types

* chore: add todos for refactoring

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-03-06 14:24:36 +05:30
M. PalanikannanandGitHub 126d01bdc5 [WEB-617] fix: link behaviour fixed on formatting (#3855)
* fix: link behaviour fixed on formatting

* chore: added harmful script checks for links
2024-03-06 14:22:02 +05:30
Bavisetti NarayanandGitHub 87eadc3c5d chore: issue link model field change (#3852) 2024-03-06 14:21:07 +05:30
53367a6bc4 [WEB-570] chore: toast refactor (#3836)
* new toast setup

* chore: new toast implementation.

* chore: move toast component to ui package.

* chore: replace `setToast` with `setPromiseToast` in required places for better UX.
* chore: code cleanup.

* chore: update theme.

* fix: theme switching issue.

* chore: remove toast from issue update operations.

* chore: add promise toast for add/ remove issue to cycle/ module and remove local spinners.

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-03-06 14:18:41 +05:30
Anmol Singh BhatiaandGitHub c06ef4d1d7 [WEB-579] style: scrollbar implementation (#3835)
* style: scrollbar added in profile summary and sidebar

* style: scrollbar added in modals

* style: scrollbar added in project setting screens

* style: scrollbar added in workspace and profile settings

* style: scrollbar added in dropdowns and filters
2024-03-06 14:18:19 +05:30
sriram veeraghanta 73334130be Merge branch 'chore/auto-merge' of github.com:makeplane/plane into preview 2024-03-06 14:07:34 +05:30
4d0f641ee0 [WEB-588] chore: remove the word title from the issue title tooltip. (#3874)
* [WEB-588] chore: remove the word `title` from the issue title tooltip.

* fix: github url fixes in feature deploy action

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-06 14:02:14 +05:30
sriram veeraghanta 50318190f5 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-03-06 12:59:24 +05:30
d07dd65022 feat: feature preview deploys for web and space nextjs applications (#3881)
* feature preview deploy

* chore: variable name changes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-03-06 12:57:14 +05:30
Henit ChobisaandGitHub f8f9dd3331 [CHANG-8] chore: Upgraded Build Pull Request CI for Faster Parallel Build with Linting Capabilities (#3838)
* chore: upgraded build pull request ci for multi stage parallel builds

* Update build-test-pull-request.yml
2024-03-05 13:14:00 +05:30
Henit ChobisaandGitHub af70722e34 chore: added workflow for checking version before merge to master (#3847) 2024-03-05 13:02:13 +05:30
Henit Chobisa 59c9b3bdce chore: added auto merge CI for merging sync branches 2024-03-04 15:13:39 +05:30
Aaryan Khandelwal bc60e7cb7f chore: icons tab interaction 2024-02-17 23:26:42 +05:30
875 changed files with 9394 additions and 8998 deletions
+97
View File
@@ -0,0 +1,97 @@
name: Auto Merge or Create PR on Push
on:
workflow_dispatch:
push:
branches:
- "sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce"
TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
REVIEWER: ${{ secrets.REVIEWER }}
jobs:
Check_Branch:
runs-on: ubuntu-latest
outputs:
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
steps:
- name: Check if current branch matches the secret
id: check-branch
run: |
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
echo "MATCH=true" >> $GITHUB_OUTPUT
else
echo "MATCH=false" >> $GITHUB_OUTPUT
fi
Auto_Merge:
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
needs: [Check_Branch]
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Setup GH CLI and Git Config
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- id: git-author
name: Setup Git CLI from Github Token
run: |
VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer')
VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}")
VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}")
VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}")
USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}"
USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com"
git config --global user.name ${USER_NAME}
git config --global user.email ${USER_EMAIL}
- name: Check for merge conflicts
id: conflicts
run: |
git fetch origin $TARGET_BRANCH
git checkout $TARGET_BRANCH
# Attempt to merge the main branch into the current branch
if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then
echo "No merge conflicts detected."
echo "HAS_CONFLICTS=false" >> $GITHUB_ENV
else
echo "Merge conflicts detected."
echo "HAS_CONFLICTS=true" >> $GITHUB_ENV
git merge --abort
fi
- name: Merge Change to Target Branch
if: env.HAS_CONFLICTS == 'false'
run: |
git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH"
git push origin $TARGET_BRANCH
- name: Create PR to Target Branch
if: env.HAS_CONFLICTS == 'true'
run: |
# Use GitHub CLI to create PR and specify author and committer
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \
--title "sync: merge conflicts need to be resolved" \
--body "" \
--reviewer $REVIEWER )
echo "Pull Request created: $PR_URL"
+84 -27
View File
@@ -1,27 +1,19 @@
name: Build Pull Request Contents
name: Build and Lint on Pull Request
on:
workflow_dispatch:
pull_request:
types: ["opened", "synchronize"]
jobs:
build-pull-request-contents:
name: Build Pull Request Contents
runs-on: ubuntu-20.04
permissions:
pull-requests: read
get-changed-files:
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }}
steps:
- name: Checkout Repository to Actions
uses: actions/checkout@v3.3.0
with:
token: ${{ secrets.ACCESS_TOKEN }}
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
- uses: actions/checkout@v3
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
@@ -31,17 +23,82 @@ jobs:
- apiserver/**
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
deploy:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
yarn
yarn build --filter=web
lint-apiserver:
needs: get-changed-files
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x' # Specify the Python version you need
- name: Install Pylint
run: python -m pip install ruff
- name: Install Apiserver Dependencies
run: cd apiserver && pip install -r requirements.txt
- name: Lint apiserver
run: ruff check --fix apiserver
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
yarn
yarn build --filter=space
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=web
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=space
build-web:
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=web
build-space:
needs: lint-space
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=space
+45
View File
@@ -0,0 +1,45 @@
name: Version Change Before Release
on:
pull_request:
branches:
- master
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Fetch base branch
run: git fetch origin master:master
- name: Get Master Branch version
run: |
git checkout master
echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Get master branch version and compare
run: |
echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION"
if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then
echo "Version in PR branch is the same as in master. Failing the CI."
exit 1
else
echo "Version check passed. Versions are different."
fi
env:
PR_VERSION: ${{ env.PR_VERSION }}
MASTER_VERSION: ${{ env.MASTER_VERSION }}
+73
View File
@@ -0,0 +1,73 @@
name: Feature Preview
on:
workflow_dispatch:
inputs:
web-build:
required: true
type: boolean
default: true
space-build:
required: true
type: boolean
default: false
jobs:
feature-deploy:
name: Feature Deploy
runs-on: ubuntu-latest
env:
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }}
BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }}
BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }}
steps:
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
- name: Kubectl Setup
run: |
curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl"
chmod +x kubectl
mkdir -p ~/.kube
echo "$KUBE_CONFIG_FILE" > ~/.kube/config
chmod 600 ~/.kube/config
- name: HELM Setup
run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
- name: App Deploy
run: |
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }}
GIT_BRANCH=${{ github.ref_name }}
APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }}
METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--kube-insecure-skip-tls-verify \
--generate-name \
--namespace $APP_NAMESPACE \
--set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \
--set shared_config.git_branch="$GIT_BRANCH" \
--set web.enabled=${{ env.BUILD_WEB }} \
--set space.enabled=${{ env.BUILD_SPACE }} \
--output json \
--timeout 1000s)
APP_NAME=$(echo $METADATA | jq -r '.name')
INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
-1
View File
@@ -50,7 +50,6 @@ chmod +x setup.sh
docker compose -f docker-compose-local.yml up
```
## Missing a Feature?
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
-1
View File
@@ -53,7 +53,6 @@ NGINX_PORT=80
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
```
## {PROJECT_FOLDER}/apiserver/.env
+17 -19
View File
@@ -44,20 +44,18 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
## ⚡ Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
| Installation Methods | Documentation Link |
|-----------------|----------------------------------------------------------------------------------------------------------|
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
| Installation Methods | Documentation Link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
## 🚀 Features
@@ -74,9 +72,7 @@ If you want more control over your data prefer to self-host Plane, please refer
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
- **Drive** (*coming soon*): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Contributors Quick Start
@@ -101,10 +97,10 @@ Setting up local environment is extremely easy and straight forward. Follow the
./setup.sh
```
5. Open the code on VSCode or similar equivalent IDE.
6. Review the `.env` files available in various folders.
6. Review the `.env` files available in various folders.
Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system.
7. Run the docker command to initiate services:
```
```
docker compose -f docker-compose-local.yml up -d
```
@@ -119,6 +115,7 @@ The Plane community can be found on [GitHub Discussions](https://github.com/orgs
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
### Repo Activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## 📸 Screenshots
@@ -181,20 +178,21 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
## ⛓️ Security
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
Email squawk@plane.so to disclose any security vulnerabilities.
## ❤️ Contribute
There are many ways to contribute to Plane, including:
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
There are many ways to contribute to Plane, including:
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
### We couldn't have done this without you.
<a href="https://github.com/makeplane/plane/graphs/contributors">
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
</a>
</a>
+27 -1
View File
@@ -1,8 +1,9 @@
from lxml import html
# Django imports
from django.utils import timezone
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
# Third party imports
from rest_framework import serializers
@@ -284,6 +285,20 @@ class IssueLinkSerializer(BaseSerializer):
"updated_at",
]
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
try:
validate_url(value)
except ValidationError:
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(('http://', 'https://')):
raise serializers.ValidationError("Invalid URL scheme.")
return value
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
@@ -295,6 +310,17 @@ class IssueLinkSerializer(BaseSerializer):
)
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data)
class IssueAttachmentSerializer(BaseSerializer):
class Meta:
+27
View File
@@ -1,5 +1,7 @@
# Django imports
from django.utils import timezone
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
# Third Party imports
from rest_framework import serializers
@@ -432,6 +434,20 @@ class IssueLinkSerializer(BaseSerializer):
"issue",
]
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
try:
validate_url(value)
except ValidationError:
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(('http://', 'https://')):
raise serializers.ValidationError("Invalid URL scheme.")
return value
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
@@ -443,6 +459,17 @@ class IssueLinkSerializer(BaseSerializer):
)
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data)
class IssueLinkLiteSerializer(BaseSerializer):
+1 -2
View File
@@ -95,8 +95,7 @@ class ProjectLiteSerializer(BaseSerializer):
"identifier",
"name",
"cover_image",
"icon_prop",
"emoji",
"logo_props",
"description",
]
read_only_fields = fields
+6
View File
@@ -22,6 +22,7 @@ from plane.app.views import (
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
ExportWorkspaceUserActivityEndpoint,
WorkspaceModulesEndpoint,
WorkspaceCyclesEndpoint,
)
@@ -191,6 +192,11 @@ urlpatterns = [
WorkspaceUserActivityEndpoint.as_view(),
name="workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-activity/<uuid:user_id>/export/",
ExportWorkspaceUserActivityEndpoint.as_view(),
name="export-workspace-user-activity",
),
path(
"workspaces/<str:slug>/user-profile/<uuid:user_id>/",
WorkspaceUserProfileEndpoint.as_view(),
+4 -1
View File
@@ -49,6 +49,7 @@ from .workspace import (
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
ExportWorkspaceUserActivityEndpoint,
WorkspaceModulesEndpoint,
WorkspaceCyclesEndpoint,
)
@@ -186,4 +187,6 @@ from .webhook import (
from .dashboard import (
DashboardEndpoint,
WidgetsEndpoint
)
)
from .error_404 import custom_404_view
+102 -86
View File
@@ -14,6 +14,7 @@ from django.db.models import (
JSONField,
Func,
Prefetch,
IntegerField,
)
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
@@ -38,6 +39,8 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
IssueRelation,
IssueAssignee,
User,
)
from plane.app.serializers import (
IssueActivitySerializer,
@@ -212,11 +215,11 @@ def dashboard_assigned_issues(self, request, slug):
if issue_type == "overdue":
overdue_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
).count()
overdue_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
)[:5]
return Response(
{
@@ -231,11 +234,11 @@ def dashboard_assigned_issues(self, request, slug):
if issue_type == "upcoming":
upcoming_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
).count()
upcoming_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
)[:5]
return Response(
{
@@ -365,11 +368,11 @@ def dashboard_created_issues(self, request, slug):
if issue_type == "overdue":
overdue_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
).count()
overdue_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now()
target_date__lt=timezone.now(),
)[:5]
return Response(
{
@@ -382,11 +385,11 @@ def dashboard_created_issues(self, request, slug):
if issue_type == "upcoming":
upcoming_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
).count()
upcoming_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now()
target_date__gte=timezone.now(),
)[:5]
return Response(
{
@@ -503,7 +506,9 @@ def dashboard_recent_projects(self, request, slug):
).exclude(id__in=unique_project_ids)
# Append additional project IDs to the existing list
unique_project_ids.update(additional_projects.values_list("id", flat=True))
unique_project_ids.update(
additional_projects.values_list("id", flat=True)
)
return Response(
list(unique_project_ids)[:4],
@@ -512,90 +517,97 @@ def dashboard_recent_projects(self, request, slug):
def dashboard_recent_collaborators(self, request, slug):
# Fetch all project IDs where the user belongs to
user_projects = Project.objects.filter(
project_projectmember__member=request.user,
project_projectmember__is_active=True,
workspace__slug=slug,
).values_list("id", flat=True)
# Fetch all users who have performed an activity in the projects where the user exists
users_with_activities = (
# Subquery to count activities for each project member
activity_count_subquery = (
IssueActivity.objects.filter(
workspace__slug=slug,
project_id__in=user_projects,
actor=OuterRef("member"),
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.values("actor")
.exclude(actor=request.user)
.annotate(num_activities=Count("actor"))
.order_by("-num_activities")
)[:7]
# Get the count of active issues for each user in users_with_activities
users_with_active_issues = []
for user_activity in users_with_activities:
user_id = user_activity["actor"]
active_issue_count = Issue.objects.filter(
assignees__in=[user_id],
state__group__in=["unstarted", "started"],
).count()
users_with_active_issues.append(
{"user_id": user_id, "active_issue_count": active_issue_count}
)
# Insert the logged-in user's ID and their active issue count at the beginning
active_issue_count = Issue.objects.filter(
assignees__in=[request.user],
state__group__in=["unstarted", "started"],
).count()
if users_with_activities.count() < 7:
# Calculate the additional collaborators needed
additional_collaborators_needed = 7 - users_with_activities.count()
# Fetch additional collaborators from the project_member table
additional_collaborators = list(
set(
ProjectMember.objects.filter(
~Q(member=request.user),
project_id__in=user_projects,
workspace__slug=slug,
)
.exclude(
member__in=[
user["actor"] for user in users_with_activities
]
)
.values_list("member", flat=True)
)
)
additional_collaborators = additional_collaborators[
:additional_collaborators_needed
]
# Append additional collaborators to the list
for collaborator_id in additional_collaborators:
active_issue_count = Issue.objects.filter(
assignees__in=[collaborator_id],
state__group__in=["unstarted", "started"],
).count()
users_with_active_issues.append(
{
"user_id": str(collaborator_id),
"active_issue_count": active_issue_count,
}
)
users_with_active_issues.insert(
0,
{"user_id": request.user.id, "active_issue_count": active_issue_count},
.annotate(num_activities=Count("pk"))
.values("num_activities")
)
return Response(users_with_active_issues, status=status.HTTP_200_OK)
# Get all project members and annotate them with activity counts
project_members_with_activities = (
ProjectMember.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.annotate(
num_activities=Coalesce(
Subquery(activity_count_subquery),
Value(0),
output_field=IntegerField(),
),
is_current_user=Case(
When(member=request.user, then=Value(0)),
default=Value(1),
output_field=IntegerField(),
),
)
.values_list("member", flat=True)
.order_by("is_current_user", "-num_activities")
.distinct()
)
search = request.query_params.get("search", None)
if search:
project_members_with_activities = (
project_members_with_activities.filter(
Q(member__display_name__icontains=search)
| Q(member__first_name__icontains=search)
| Q(member__last_name__icontains=search)
)
)
return self.paginate(
request=request,
queryset=project_members_with_activities,
controller=self.get_results_controller,
)
class DashboardEndpoint(BaseAPIView):
def get_results_controller(self, project_members_with_activities):
user_active_issue_counts = (
User.objects.filter(id__in=project_members_with_activities)
.annotate(
active_issue_count=Count(
Case(
When(
issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
then=1,
),
output_field=IntegerField(),
)
)
)
.values("active_issue_count", user_id=F("id"))
)
# Create a dictionary to store the active issue counts by user ID
active_issue_counts_dict = {
user["user_id"]: user["active_issue_count"]
for user in user_active_issue_counts
}
# Preserve the sequence of project members with activities
paginated_results = [
{
"user_id": member_id,
"active_issue_count": active_issue_counts_dict.get(
member_id, 0
),
}
for member_id in project_members_with_activities
]
return paginated_results
def create(self, request, slug):
serializer = DashboardSerializer(data=request.data)
if serializer.is_valid():
@@ -622,7 +634,9 @@ class DashboardEndpoint(BaseAPIView):
dashboard_type = request.GET.get("dashboard_type", None)
if dashboard_type == "home":
dashboard, created = Dashboard.objects.get_or_create(
type_identifier=dashboard_type, owned_by=request.user, is_default=True
type_identifier=dashboard_type,
owned_by=request.user,
is_default=True,
)
if created:
@@ -639,7 +653,9 @@ class DashboardEndpoint(BaseAPIView):
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True)
widget = Widget.objects.filter(
key=widget_key
).values_list("id", flat=True)
if widget:
updated_dashboard_widgets.append(
DashboardWidget(
+5
View File
@@ -0,0 +1,5 @@
# views.py
from django.http import JsonResponse
def custom_404_view(request, exception=None):
return JsonResponse({"error": "Page not found."}, status=404)
+63 -4
View File
@@ -1,9 +1,12 @@
# Python imports
import jwt
import csv
import io
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
# Django imports
from django.http import HttpResponse
from django.db import IntegrityError
from django.conf import settings
from django.utils import timezone
@@ -1238,6 +1241,66 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
)
class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def generate_csv_from_rows(self, rows):
"""Generate CSV buffer from rows."""
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
[writer.writerow(row) for row in rows]
csv_buffer.seek(0)
return csv_buffer
def post(self, request, slug, user_id):
if not request.data.get("date"):
return Response(
{"error": "Date is required"},
status=status.HTTP_400_BAD_REQUEST,
)
user_activities = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug,
created_at__date=request.data.get("date"),
project__project_projectmember__member=request.user,
actor_id=user_id,
).select_related("actor", "workspace", "issue", "project")[:10000]
header = [
"Actor name",
"Issue ID",
"Project",
"Created at",
"Updated at",
"Action",
"Field",
"Old value",
"New value",
]
rows = [
(
activity.actor.display_name,
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
activity.project.name,
activity.created_at,
activity.updated_at,
activity.verb,
activity.field,
activity.old_value,
activity.new_value,
)
for activity in user_activities
]
csv_buffer = self.generate_csv_from_rows([header] + rows)
response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"'
return response
class WorkspaceUserProfileEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
user_data = User.objects.get(pk=user_id)
@@ -1303,10 +1366,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
)
.values(
"id",
"name",
"identifier",
"emoji",
"icon_prop",
"created_issues",
"assigned_issues",
"completed_issues",
@@ -0,0 +1,54 @@
# Generated by Django 4.2.7 on 2024-03-03 16:25
from django.db import migrations, models
class Migration(migrations.Migration):
def update_project_logo_props(apps, schema_editor):
Project = apps.get_model("db", "Project")
bulk_update_project_logo = []
# Iterate through projects and update logo_props
for project in Project.objects.all():
project.logo_props["in_use"] = "emoji" if project.emoji else "icon"
project.logo_props["emoji"] = {
"value": project.emoji if project.emoji else "",
"url": "",
}
project.logo_props["icon"] = {
"name": (
project.icon_prop.get("name", "")
if project.icon_prop
else ""
),
"color": (
project.icon_prop.get("color", "")
if project.icon_prop
else ""
),
}
bulk_update_project_logo.append(project)
# Bulk update logo_props for all projects
Project.objects.bulk_update(
bulk_update_project_logo, ["logo_props"], batch_size=1000
)
dependencies = [
("db", "0060_cycle_progress_snapshot"),
]
operations = [
migrations.AlterField(
model_name="issuelink",
name="url",
field=models.TextField(),
),
migrations.AddField(
model_name="project",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.RunPython(update_project_logo_props),
]
+1 -1
View File
@@ -320,7 +320,7 @@ class IssueAssignee(ProjectBaseModel):
class IssueLink(ProjectBaseModel):
title = models.CharField(max_length=255, null=True, blank=True)
url = models.URLField()
url = models.TextField()
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_link"
)
+1
View File
@@ -107,6 +107,7 @@ class Project(BaseModel):
close_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
)
logo_props = models.JSONField(default=dict)
default_state = models.ForeignKey(
"db.State",
on_delete=models.SET_NULL,
+1
View File
@@ -7,6 +7,7 @@ from django.views.generic import TemplateView
from django.conf import settings
handler404 = "plane.app.views.error_404.custom_404_view"
urlpatterns = [
path("", TemplateView.as_view(template_name="index.html")),
+17 -13
View File
@@ -31,11 +31,11 @@ curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-cl
```
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
</details>
--
Expect this after a successful install
![Install Output](images/install.png)
@@ -50,29 +50,33 @@ Plane App is available via the command `plane-app`. Running the command `plane-a
![Plane Help](images/help.png)
<ins>Basic Operations</ins>:
<ins>Basic Operations</ins>:
1. Start Server using `plane-app start`
1. Stop Server using `plane-app stop`
1. Restart Server using `plane-app restart`
<ins>Advanced Operations</ins>:
1. Configure Plane using `plane-app --configure`. This will give you options to modify
- NGINX Port (default 80)
- Domain Name (default is the local server public IP address)
- File Upload Size (default 5MB)
- External Postgres DB Url (optional - default empty)
- External Redis URL (optional - default empty)
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
- NGINX Port (default 80)
- Domain Name (default is the local server public IP address)
- File Upload Size (default 5MB)
- External Postgres DB Url (optional - default empty)
- External Redis URL (optional - default empty)
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
1. Plane App can be reinstalled using `plane-app --install`.
1. Plane App can be reinstalled using `plane-app --install`.
<ins>Application Data is stored in the mentioned folders</ins>:
<ins>Application Data is stored in the mentioned folders</ins>:
1. DB Data: /opt/plane/data/postgres
1. Redis Data: /opt/plane/data/redis
1. Minio Data: /opt/plane/data/minio
1. Minio Data: /opt/plane/data/minio
+1 -2
View File
@@ -59,8 +59,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "^7.32.0",
"eslint-config-next": "13.2.4",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"tsconfig": "*",
+3 -3
View File
@@ -98,7 +98,7 @@
top: 0;
bottom: -2px;
width: 4px;
z-index: 99;
z-index: 5;
background-color: #d9e4ff;
pointer-events: none;
}
@@ -111,7 +111,7 @@
.tableWrapper .tableControls .rowsControl {
transition: opacity ease-in 100ms;
position: absolute;
z-index: 99;
z-index: 5;
display: flex;
justify-content: center;
align-items: center;
@@ -198,7 +198,7 @@
.tableWrapper .tableControls .tableToolbox .toolboxItem:hover,
.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover {
background-color: rgba(var(--color-background-100), 0.5);
background-color: rgba(var(--color-background-80), 0.6);
}
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer,
@@ -15,9 +15,15 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
return false;
}
const eventTarget = event.target as HTMLElement;
let a = event.target as HTMLElement;
const els = [];
if (eventTarget.nodeName !== "A") {
while (a.nodeName !== "DIV") {
els.push(a);
a = a.parentNode as HTMLElement;
}
if (!els.find((value) => value.nodeName === "A")) {
return false;
}
@@ -28,9 +34,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
const target = link?.target ?? attrs.target;
if (link && href) {
if (view.editable) {
window.open(href, target);
}
window.open(href, target);
return true;
}
@@ -33,16 +33,8 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin {
return false;
}
const html = event.clipboardData?.getData("text/html");
const hrefRegex = /href="([^"]*)"/;
const existingLink = html?.match(hrefRegex);
const url = existingLink ? existingLink[1] : link.href;
options.editor.commands.setMark(options.type, {
href: url,
href: link.href,
});
return true;
@@ -1,41 +1,76 @@
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core";
import { Plugin } from "@tiptap/pm/state";
import { find, registerCustomProtocol, reset } from "linkifyjs";
import { autolink } from "src/ui/extensions/custom-link/helpers/autolink";
import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler";
import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler";
import { autolink } from "./helpers/autolink";
import { clickHandler } from "./helpers/clickHandler";
import { pasteHandler } from "./helpers/pasteHandler";
export interface LinkProtocolOptions {
scheme: string;
optionalSlashes?: boolean;
}
export const pasteRegex =
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
export interface LinkOptions {
/**
* If enabled, it adds links as you type.
*/
autolink: boolean;
inclusive: boolean;
/**
* An array of custom protocols to be registered with linkifyjs.
*/
protocols: Array<LinkProtocolOptions | string>;
/**
* If enabled, links will be opened on click.
*/
openOnClick: boolean;
/**
* If enabled, links will be inclusive i.e. if you move your cursor to the
* link text, and start typing, it'll be a part of the link itself.
*/
inclusive: boolean;
/**
* Adds a link to the current selection if the pasted content only contains an url.
*/
linkOnPaste: boolean;
/**
* A list of HTML attributes to be rendered.
*/
HTMLAttributes: Record<string, any>;
/**
* A validation function that modifies link verification for the auto linker.
* @param url - The url to be validated.
* @returns - True if the url is valid, false otherwise.
*/
validate?: (url: string) => boolean;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
link: {
/**
* Set a link mark
*/
setLink: (attributes: {
href: string;
target?: string | null;
rel?: string | null;
class?: string | null;
}) => ReturnType;
/**
* Toggle a link mark
*/
toggleLink: (attributes: {
href: string;
target?: string | null;
rel?: string | null;
class?: string | null;
}) => ReturnType;
/**
* Unset a link mark
*/
unsetLink: () => ReturnType;
};
}
@@ -150,37 +185,31 @@ export const CustomLinkExtension = Mark.create<LinkOptions>({
addPasteRules() {
return [
markPasteRule({
find: (text) =>
find(text)
.filter((link) => {
if (this.options.validate) {
return this.options.validate(link.value);
}
return true;
})
.filter((link) => link.isLink)
.map((link) => ({
text: link.value,
index: link.start,
data: link,
})),
type: this.type,
getAttributes: (match, pasteEvent) => {
const html = pasteEvent?.clipboardData?.getData("text/html");
const hrefRegex = /href="([^"]*)"/;
find: (text) => {
const foundLinks: PasteRuleMatch[] = [];
const existingLink = html?.match(hrefRegex);
if (text) {
const links = find(text).filter((item) => item.isLink);
if (existingLink) {
return {
href: existingLink[1],
};
if (links.length) {
links.forEach((link) =>
foundLinks.push({
text: link.value,
data: {
href: link.href,
},
index: link.start,
})
);
}
}
return {
href: match.data?.href,
};
return foundLinks;
},
type: this.type,
getAttributes: (match) => ({
href: match.data?.href,
}),
}),
];
},
@@ -0,0 +1,111 @@
import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core";
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
export interface HorizontalRuleOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
horizontalRule: {
/**
* Add a horizontal rule
*/
setHorizontalRule: () => ReturnType;
};
}
}
export const CustomHorizontalRule = Node.create<HorizontalRuleOptions>({
name: "horizontalRule",
addOptions() {
return {
HTMLAttributes: {},
};
},
group: "block",
parseHTML() {
return [{ tag: "hr" }];
},
renderHTML({ HTMLAttributes }) {
return ["hr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
addCommands() {
return {
setHorizontalRule:
() =>
({ chain, state }) => {
const { selection } = state;
const { $from: $originFrom, $to: $originTo } = selection;
const currentChain = chain();
if ($originFrom.parentOffset === 0) {
currentChain.insertContentAt(
{
from: Math.max($originFrom.pos - 1, 0),
to: $originTo.pos,
},
{
type: this.name,
}
);
} else if (isNodeSelection(selection)) {
currentChain.insertContentAt($originTo.pos, {
type: this.name,
});
} else {
currentChain.insertContent({ type: this.name });
}
return (
currentChain
// set cursor after horizontal rule
.command(({ tr, dispatch }) => {
if (dispatch) {
const { $to } = tr.selection;
const posAfter = $to.end();
if ($to.nodeAfter) {
if ($to.nodeAfter.isTextblock) {
tr.setSelection(TextSelection.create(tr.doc, $to.pos + 1));
} else if ($to.nodeAfter.isBlock) {
tr.setSelection(NodeSelection.create(tr.doc, $to.pos));
} else {
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
}
} else {
// add node after horizontal rule if its the end of the document
const node = $to.parent.type.contentMatch.defaultType?.create();
if (node) {
tr.insert(posAfter, node);
tr.setSelection(TextSelection.create(tr.doc, posAfter + 1));
}
}
tr.scrollIntoView();
}
return true;
})
.run()
);
},
};
},
addInputRules() {
return [
nodeInputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
type: this.type,
}),
];
},
});
@@ -27,6 +27,7 @@ import { RestoreImage } from "src/types/restore-image";
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
import { CustomTypographyExtension } from "src/ui/extensions/typography";
import { CustomHorizontalRule } from "./horizontal-rule/horizontal-rule";
export const CoreEditorExtensions = (
mentionConfig: {
@@ -55,9 +56,7 @@ export const CoreEditorExtensions = (
},
code: false,
codeBlock: false,
horizontalRule: {
HTMLAttributes: { class: "mt-4 mb-4" },
},
horizontalRule: false,
blockquote: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
@@ -67,6 +66,10 @@ export const CoreEditorExtensions = (
CustomQuoteExtension.configure({
HTMLAttributes: { className: "border-l-4 border-custom-border-300" },
}),
CustomHorizontalRule.configure({
HTMLAttributes: { class: "mt-4 mb-4" },
}),
CustomKeymap,
ListKeymap,
CustomLinkExtension.configure({
@@ -213,10 +213,11 @@ function createToolbox({
{ className: "colorPicker grid" },
Object.entries(colors).map(([colorName, colorValue]) =>
h("div", {
className: "colorPickerItem",
className: "colorPickerItem flex items-center justify-center",
style: `background-color: ${colorValue.backgroundColor};
color: ${colorValue.textColor || "inherit"};`,
innerHTML: colorValue?.icon || "",
color: ${colorValue.textColor || "inherit"};`,
innerHTML:
colorValue.icon ?? `<span class="text-md" style:"color: ${colorValue.backgroundColor}>A</span>`,
onClick: () => onSelectColor(colorValue),
})
)
+1 -2
View File
@@ -37,7 +37,6 @@
"@tiptap/extension-placeholder": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"eslint-config-next": "13.2.4",
"lucide-react": "^0.309.0",
"react-popper": "^2.3.0",
"tippy.js": "^6.3.7",
@@ -47,7 +46,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "8.36.0",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"tsconfig": "*",
@@ -15,7 +15,7 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
const handleOnClick = (marking: IMarking) => {
scrollSummary(editor, marking);
if (setSidePeekVisible) setSidePeekVisible(false);
}
};
return (
<div className="flex h-full flex-col overflow-hidden">
@@ -33,8 +33,9 @@ export const SummaryPopover: React.FC<Props> = (props) => {
<button
type="button"
ref={setReferenceElement}
className={`grid h-7 w-7 place-items-center rounded ${sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
className={`grid h-7 w-7 place-items-center rounded ${
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
onClick={() => setSidePeekVisible(!sidePeekVisible)}
>
<List className="h-4 w-4" />
@@ -26,4 +26,3 @@ export const DocumentEditorExtensions = (
}),
IssueWidgetPlaceholder(),
];
+1 -2
View File
@@ -33,7 +33,6 @@
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"eslint-config-next": "13.2.4",
"lucide-react": "^0.294.0",
"tippy.js": "^6.3.7"
},
@@ -41,7 +40,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "8.36.0",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"tsconfig": "*",
@@ -36,10 +36,9 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",
"eslint-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
"typescript": "4.9.5"
@@ -39,7 +39,7 @@
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint": "^7.32.0",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"react": "^18.2.0",
"tailwind-config-custom": "*",
+27 -6
View File
@@ -1,22 +1,43 @@
module.exports = {
extends: ["next", "turbo", "prettier"],
extends: [
"next",
"turbo",
"prettier",
"plugin:@typescript-eslint/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["react", "@typescript-eslint"],
parserOptions: {
ecmaVersion: 2021, // Or the ECMAScript version you are using
sourceType: "module", // Or 'script' if you're using CommonJS or other modules
},
plugins: ["react", "@typescript-eslint", "import"],
settings: {
next: {
rootDir: ["web/", "space/", "packages/*/"],
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
"prefer-const": "error",
"no-irregular-whitespace": "error",
"no-trailing-spaces": "error",
"no-duplicate-imports": "error",
"arrow-body-style": ["error", "as-needed"],
"react/self-closing-comp": ["error", { component: true, html: true }],
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": ["warn"],
"react/jsx-key": "error",
"react/self-closing-comp": ["error", { component: true, html: true }],
"react/jsx-boolean-value": "error",
"react/jsx-no-duplicate-props": "error",
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-useless-empty-export": "error",
"@typescript-eslint/prefer-ts-expect-error": "error",
"@typescript-eslint/naming-convention": [
"error",
{
selector: ["function", "variable"],
format: ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
},
],
},
};
+10 -12
View File
@@ -4,18 +4,16 @@
"version": "0.16.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {},
"dependencies": {
"eslint": "^7.23.0",
"eslint-config-next": "13.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-turbo": "latest",
"eslint-plugin-react": "7.31.8"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.13.2",
"typescript": "^4.7.4"
},
"publishConfig": {
"access": "public"
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"eslint": "^8.57.0",
"eslint-config-next": "^14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^1.12.4",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.33.2",
"typescript": "^5.3.3"
}
}
@@ -198,6 +198,31 @@ module.exports = {
300: convertToRGB("--color-onboarding-border-300"),
},
},
toast: {
text: {
success: convertToRGB("--color-toast-success-text"),
error: convertToRGB("--color-toast-error-text"),
warning: convertToRGB("--color-toast-warning-text"),
info: convertToRGB("--color-toast-info-text"),
loading: convertToRGB("--color-toast-loading-text"),
secondary: convertToRGB("--color-toast-secondary-text"),
tertiary: convertToRGB("--color-toast-tertiary-text"),
},
background: {
success: convertToRGB("--color-toast-success-background"),
error: convertToRGB("--color-toast-error-background"),
warning: convertToRGB("--color-toast-warning-background"),
info: convertToRGB("--color-toast-info-background"),
loading: convertToRGB("--color-toast-loading-background"),
},
border: {
success: convertToRGB("--color-toast-success-border"),
error: convertToRGB("--color-toast-error-border"),
warning: convertToRGB("--color-toast-warning-border"),
info: convertToRGB("--color-toast-info-border"),
loading: convertToRGB("--color-toast-loading-border"),
},
},
},
keyframes: {
leftToaster: {
+1 -8
View File
@@ -1,11 +1,4 @@
import type {
IUser,
TIssue,
IProjectLite,
IWorkspaceLite,
IIssueFilterOptions,
IUserLite,
} from "@plane/types";
import type { TIssue, IIssueFilterOptions } from "@plane/types";
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
@@ -3,6 +3,15 @@ import { TIssue } from "./issues/issue";
import { TIssueRelationTypes } from "./issues/issue_relation";
import { TStateGroups } from "./state";
enum EDurationFilters {
NONE = "none",
TODAY = "today",
THIS_WEEK = "this_week",
THIS_MONTH = "this_month",
THIS_YEAR = "this_year",
CUSTOM = "custom",
}
export type TWidgetKeys =
| "overview_stats"
| "assigned_issues"
@@ -15,30 +24,27 @@ export type TWidgetKeys =
export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed";
export type TDurationFilterOptions =
| "none"
| "today"
| "this_week"
| "this_month"
| "this_year";
// widget filters
export type TAssignedIssuesWidgetFilters = {
duration?: TDurationFilterOptions;
custom_dates?: string[];
duration?: EDurationFilters;
tab?: TIssuesListTypes;
};
export type TCreatedIssuesWidgetFilters = {
duration?: TDurationFilterOptions;
custom_dates?: string[];
duration?: EDurationFilters;
tab?: TIssuesListTypes;
};
export type TIssuesByStateGroupsWidgetFilters = {
duration?: TDurationFilterOptions;
duration?: EDurationFilters;
custom_dates?: string[];
};
export type TIssuesByPriorityWidgetFilters = {
duration?: TDurationFilterOptions;
custom_dates?: string[];
duration?: EDurationFilters;
};
export type TWidgetFiltersFormData =
@@ -97,6 +103,12 @@ export type TWidgetStatsRequestParams =
| {
target_date: string;
widget_key: "issues_by_priority";
}
| {
cursor: string;
per_page: number;
search?: string;
widget_key: "recent_collaborators";
};
export type TWidgetIssue = TIssue & {
@@ -141,8 +153,17 @@ export type TRecentActivityWidgetResponse = IIssueActivity;
export type TRecentProjectsWidgetResponse = string[];
export type TRecentCollaboratorsWidgetResponse = {
active_issue_count: number;
user_id: string;
count: number;
extra_stats: Object | null;
next_cursor: string;
next_page_results: boolean;
prev_cursor: string;
prev_page_results: boolean;
results: {
active_issue_count: number;
user_id: string;
}[];
total_pages: number;
};
export type TWidgetStatsResponse =
@@ -153,7 +174,7 @@ export type TWidgetStatsResponse =
| TCreatedIssuesWidgetResponse
| TRecentActivityWidgetResponse[]
| TRecentProjectsWidgetResponse
| TRecentCollaboratorsWidgetResponse[];
| TRecentCollaboratorsWidgetResponse;
// dashboard
export type TDashboard = {
+6
View File
@@ -0,0 +1,6 @@
export enum EUserProjectRoles {
GUEST = 5,
VIEWER = 10,
MEMBER = 15,
ADMIN = 20,
}
@@ -1,5 +1,5 @@
import { TIssue } from "./issues/base";
import type { IProjectLite } from "./projects";
import { TIssue } from "../issues/base";
import type { IProjectLite } from "../projects";
export type TInboxIssueExtended = {
completed_at: string | null;
@@ -33,34 +33,6 @@ export interface IInbox {
workspace: string;
}
interface StatePending {
readonly status: -2;
}
interface StatusReject {
status: -1;
}
interface StatusSnoozed {
status: 0;
snoozed_till: Date;
}
interface StatusAccepted {
status: 1;
}
interface StatusDuplicate {
status: 2;
duplicate_to: string;
}
export type TInboxStatus =
| StatusReject
| StatusSnoozed
| StatusAccepted
| StatusDuplicate
| StatePending;
export interface IInboxFilterOptions {
priority?: string[] | null;
inbox_status?: number[] | null;
+2 -1
View File
@@ -1,2 +1,3 @@
export * from "./inbox";
export * from "./inbox-issue";
export * from "./inbox-types";
export * from "./inbox";
-10
View File
@@ -4,7 +4,6 @@ export * from "./cycles";
export * from "./dashboard";
export * from "./projects";
export * from "./state";
export * from "./invitation";
export * from "./issues";
export * from "./modules";
export * from "./views";
@@ -15,7 +14,6 @@ export * from "./estimate";
export * from "./importer";
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
export * from "./inbox";
export * from "./inbox/root";
export * from "./analytics";
@@ -31,11 +29,3 @@ export * from "./auth";
export * from "./api_token";
export * from "./instance";
export * from "./app";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends { pop: any; push: any }
? `${Key}`
: `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
+14 -14
View File
@@ -1,16 +1,12 @@
import type {
IUser,
IUserLite,
TIssue,
IProject,
IWorkspace,
IWorkspaceLite,
IProjectLite,
IIssueFilterOptions,
ILinkDetails,
} from "@plane/types";
import type { TIssue, IIssueFilterOptions, ILinkDetails } from "@plane/types";
export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
export type TModuleStatus =
| "backlog"
| "planned"
| "in-progress"
| "paused"
| "completed"
| "cancelled";
export interface IModule {
backlog_issues: number;
@@ -68,6 +64,10 @@ export type ModuleLink = {
url: string;
};
export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectModuleType =
| (IModule & { actionType: "edit" | "delete" | "create-issue" })
| undefined;
export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined;
export type SelectIssue =
| (TIssue & { actionType: "edit" | "delete" | "create" })
| undefined;
+6 -1
View File
@@ -1,5 +1,10 @@
// types
import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types";
import {
TIssue,
IIssueLabel,
IWorkspaceLite,
IProjectLite,
} from "@plane/types";
export interface IPage {
access: number;
+15 -12
View File
@@ -1,12 +1,26 @@
import { EUserProjectRoles } from "constants/project";
import type {
IProjectViewProps,
IUser,
IUserLite,
IUserMemberLite,
IWorkspace,
IWorkspaceLite,
TStateGroups,
} from ".";
export type TProjectLogoProps = {
in_use: "emoji" | "icon";
emoji?: {
value?: string;
url?: string;
};
icon?: {
name?: string;
color?: string;
};
};
export interface IProject {
archive_in: number;
close_in: number;
@@ -21,24 +35,13 @@ export interface IProject {
default_assignee: IUser | string | null;
default_state: string | null;
description: string;
emoji: string | null;
emoji_and_icon:
| string
| {
name: string;
color: string;
}
| null;
estimate: string | null;
icon_prop: {
name: string;
color: string;
} | null;
id: string;
identifier: string;
is_deployed: boolean;
is_favorite: boolean;
is_member: boolean;
logo_props: TProjectLogoProps;
member_role: EUserProjectRoles | null;
members: IProjectMemberLite[];
name: string;
+6 -1
View File
@@ -1,6 +1,11 @@
import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types";
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
export type TStateGroups =
| "backlog"
| "unstarted"
| "started"
| "completed"
| "cancelled";
export interface IState {
readonly id: string;
+7 -23
View File
@@ -1,5 +1,9 @@
import { EUserProjectRoles } from "constants/project";
import { IIssueActivity, IIssueLite, TStateGroups } from ".";
import {
IIssueActivity,
TIssuePriorities,
TStateGroups,
EUserProjectRoles,
} from ".";
export interface IUser {
id: string;
@@ -17,7 +21,6 @@ export interface IUser {
is_onboarded: boolean;
is_password_autoset: boolean;
is_tour_completed: boolean;
is_password_autoset: boolean;
mobile_number: string | null;
role: string | null;
onboarding_step: {
@@ -80,7 +83,7 @@ export interface IUserActivity {
}
export interface IUserPriorityDistribution {
priority: string;
priority: TIssuePriorities;
priority_count: number;
}
@@ -89,21 +92,6 @@ export interface IUserStateDistribution {
state_count: number;
}
export interface IUserWorkspaceDashboard {
assigned_issues_count: number;
completed_issues_count: number;
issue_activities: IUserActivity[];
issues_due_week_count: number;
overdue_issues: IIssueLite[];
completed_issues: {
week_in_month: number;
completed_count: number;
}[];
pending_issues_count: number;
state_distribution: IUserStateDistribution[];
upcoming_issues: IIssueLite[];
}
export interface IUserActivityResponse {
count: number;
extra_stats: null;
@@ -144,11 +132,7 @@ export interface IUserProfileProjectSegregation {
assigned_issues: number;
completed_issues: number;
created_issues: number;
emoji: string | null;
icon_prop: null;
id: string;
identifier: string;
name: string;
pending_issues: number;
}[];
user_data: {
+5 -1
View File
@@ -1,4 +1,8 @@
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
} from "./view-props";
export interface IProjectView {
id: string;
+2
View File
@@ -23,9 +23,11 @@
"@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0",
"sonner": "^1.4.2",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
+5 -5
View File
@@ -122,14 +122,14 @@ export const badgeStyling: IBadgeStyling = {
};
export const getBadgeStyling = (variant: TBadgeVariant, size: TBadgeSizes, disabled: boolean = false): string => {
let _variant: string = ``;
let tempVariant: string = ``;
const currentVariant = badgeStyling[variant];
_variant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover}`;
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover}`;
let _size: string = ``;
if (size) _size = badgeSizeStyling[size];
return `${_variant} ${_size}`;
let tempSize: string = ``;
if (size) tempSize = badgeSizeStyling[size];
return `${tempVariant} ${tempSize}`;
};
export const getIconStyling = (size: TBadgeSizes): string => {
+8 -7
View File
@@ -29,13 +29,10 @@ const Breadcrumbs = ({ children, onBack }: BreadcrumbsProps) => {
<React.Fragment key={index}>
{index > 0 && !isSmallScreen && (
<div className="flex items-center gap-2.5">
<ChevronRight
className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400"
aria-hidden="true"
/>
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div>
)}
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? 'hidden sm:flex' : 'flex'}`}>
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? "hidden sm:flex" : "flex"}`}>
{child}
</div>
</React.Fragment>
@@ -46,7 +43,11 @@ const Breadcrumbs = ({ children, onBack }: BreadcrumbsProps) => {
{isSmallScreen && childrenArray.length > 1 && (
<>
<div className="flex items-center gap-2.5">
{onBack && <span onClick={onBack} className="text-custom-text-200">...</span>}
{onBack && (
<span onClick={onBack} className="text-custom-text-200">
...
</span>
)}
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div>
<div className="flex items-center gap-2.5">{childrenArray[childrenArray.length - 1]}</div>
@@ -70,4 +71,4 @@ const BreadcrumbItem: React.FC<Props> = (props) => {
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
export { Breadcrumbs, BreadcrumbItem };
export { Breadcrumbs, BreadcrumbItem };
+5 -5
View File
@@ -100,16 +100,16 @@ export const buttonStyling: IButtonStyling = {
};
export const getButtonStyling = (variant: TButtonVariant, size: TButtonSizes, disabled: boolean = false): string => {
let _variant: string = ``;
let tempVariant: string = ``;
const currentVariant = buttonStyling[variant];
_variant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
currentVariant.pressed
}`;
let _size: string = ``;
if (size) _size = buttonSizeStyling[size];
return `${_variant} ${_size}`;
let tempSize: string = ``;
if (size) tempSize = buttonSizeStyling[size];
return `${tempVariant} ${tempSize}`;
};
export const getIconStyling = (size: TButtonSizes): string => {
@@ -12,7 +12,7 @@ export const ControlLink: React.FC<TControlLink> = (props) => {
const { href, onClick, children, target = "_self", disabled = false, ...rest } = props;
const LEFT_CLICK_EVENT_CODE = 0;
const _onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
const clickCondition = (event.metaKey || event.ctrlKey) && event.button === LEFT_CLICK_EVENT_CODE;
if (!clickCondition) {
event.preventDefault();
@@ -23,7 +23,7 @@ export const ControlLink: React.FC<TControlLink> = (props) => {
if (disabled) return <>{children}</>;
return (
<a href={href} target={target} onClick={_onClick} {...rest}>
<a href={href} target={target} onClick={handleOnClick} {...rest}>
{children}
</a>
);
+169
View File
@@ -0,0 +1,169 @@
import React, { useState } from "react";
import { usePopper } from "react-popper";
import EmojiPicker, { EmojiClickData, Theme } from "emoji-picker-react";
import { Popover, Tab } from "@headlessui/react";
import { Placement } from "@popperjs/core";
// components
import { IconsList } from "./icons-list";
// helpers
import { cn } from "../../helpers";
export enum EmojiIconPickerTypes {
EMOJI = "emoji",
ICON = "icon",
}
type TChangeHandlerProps =
| {
type: EmojiIconPickerTypes.EMOJI;
value: EmojiClickData;
}
| {
type: EmojiIconPickerTypes.ICON;
value: {
name: string;
color: string;
};
};
export type TCustomEmojiPicker = {
buttonClassName?: string;
className?: string;
closeOnSelect?: boolean;
defaultIconColor?: string;
defaultOpen?: EmojiIconPickerTypes;
disabled?: boolean;
dropdownClassName?: string;
label: React.ReactNode;
onChange: (value: TChangeHandlerProps) => void;
placement?: Placement;
searchPlaceholder?: string;
theme?: Theme;
};
const TABS_LIST = [
{
key: EmojiIconPickerTypes.EMOJI,
title: "Emojis",
},
{
key: EmojiIconPickerTypes.ICON,
title: "Icons",
},
];
export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
const {
buttonClassName,
className,
closeOnSelect = true,
defaultIconColor = "#5f5f5f",
defaultOpen = EmojiIconPickerTypes.EMOJI,
disabled = false,
dropdownClassName,
label,
onChange,
placement = "bottom-start",
searchPlaceholder = "Search",
theme,
} = props;
// refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
{
name: "preventOverflow",
options: {
padding: 20,
},
},
],
});
return (
<Popover as="div" className={cn("relative", className)}>
{({ close }) => (
<>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={cn("outline-none", buttonClassName)}
disabled={disabled}
>
{label}
</button>
</Popover.Button>
<Popover.Panel className="fixed z-10">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn(
"h-80 w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
dropdownClassName
)}
>
<Tab.Group
as="div"
className="h-full w-full flex flex-col overflow-hidden"
defaultIndex={TABS_LIST.findIndex((tab) => tab.key === defaultOpen)}
>
<Tab.List as="div" className="grid grid-cols-2 gap-1 p-2">
{TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
cn("py-1 text-sm rounded border border-custom-border-200", {
"bg-custom-background-80": selected,
"hover:bg-custom-background-90 focus:bg-custom-background-90": !selected,
})
}
>
{tab.title}
</Tab>
))}
</Tab.List>
<Tab.Panels as="div" className="h-full w-full overflow-y-auto">
<Tab.Panel>
<EmojiPicker
onEmojiClick={(val) => {
onChange({
type: EmojiIconPickerTypes.EMOJI,
value: val,
});
if (closeOnSelect) close();
}}
height="20rem"
width="100%"
theme={theme}
searchPlaceholder={searchPlaceholder}
previewConfig={{
showPreview: false,
}}
/>
</Tab.Panel>
<Tab.Panel>
<IconsList
defaultColor={defaultIconColor}
onChange={(val) => {
onChange({
type: EmojiIconPickerTypes.ICON,
value: val,
});
if (closeOnSelect) close();
}}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
</>
)}
</Popover>
);
};
+110
View File
@@ -0,0 +1,110 @@
import React, { useEffect, useState } from "react";
// components
import { Input } from "../form-fields";
// helpers
import { cn } from "../../helpers";
// constants
import { MATERIAL_ICONS_LIST } from "./icons";
type TIconsListProps = {
defaultColor: string;
onChange: (val: { name: string; color: string }) => void;
};
const DEFAULT_COLORS = ["#ff6b00", "#8cc1ff", "#fcbe1d", "#18904f", "#adf672", "#05c3ff", "#5f5f5f"];
export const IconsList: React.FC<TIconsListProps> = (props) => {
const { defaultColor, onChange } = props;
// states
const [activeColor, setActiveColor] = useState(defaultColor);
const [showHexInput, setShowHexInput] = useState(false);
const [hexValue, setHexValue] = useState("");
useEffect(() => {
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false);
else {
setHexValue(defaultColor.slice(1, 7));
setShowHexInput(true);
}
}, [defaultColor]);
return (
<>
<div className="grid grid-cols-8 gap-2 items-center justify-items-center px-2.5 h-9">
{showHexInput ? (
<div className="col-span-7 flex items-center gap-1 justify-self-stretch ml-2">
<span
className="h-4 w-4 flex-shrink-0 rounded-full mr-1"
style={{
backgroundColor: `#${hexValue}`,
}}
/>
<span className="text-xs text-custom-text-300 flex-shrink-0">HEX</span>
<span className="text-xs text-custom-text-200 flex-shrink-0 -mr-1">#</span>
<Input
type="text"
value={hexValue}
onChange={(e) => {
const value = e.target.value;
setHexValue(value);
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`);
}}
className="flex-grow pl-0 text-xs text-custom-text-200"
mode="true-transparent"
autoFocus
/>
</div>
) : (
DEFAULT_COLORS.map((curCol) => (
<button
key={curCol}
type="button"
className="grid place-items-center"
onClick={() => {
setActiveColor(curCol);
setHexValue(curCol.slice(1, 7));
}}
>
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
</button>
))
)}
<button
type="button"
className={cn("grid place-items-center h-4 w-4 rounded-full border border-transparent", {
"border-custom-border-400": !showHexInput,
})}
onClick={() => {
setShowHexInput((prevData) => !prevData);
setHexValue(activeColor.slice(1, 7));
}}
>
{showHexInput ? (
<span className="conical-gradient h-4 w-4 rounded-full" />
) : (
<span className="text-custom-text-300 text-[0.6rem] grid place-items-center">#</span>
)}
</button>
</div>
<div className="grid grid-cols-8 gap-2 px-2.5 justify-items-center mt-2">
{MATERIAL_ICONS_LIST.map((icon) => (
<button
key={icon.name}
type="button"
className="h-6 w-6 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
onClick={() => {
onChange({
name: icon.name,
color: activeColor,
});
}}
>
<span style={{ color: activeColor }} className="material-symbols-rounded text-base">
{icon.name}
</span>
</button>
))}
</div>
</>
);
};
+605
View File
@@ -0,0 +1,605 @@
export const MATERIAL_ICONS_LIST = [
{
name: "search",
},
{
name: "home",
},
{
name: "menu",
},
{
name: "close",
},
{
name: "settings",
},
{
name: "done",
},
{
name: "check_circle",
},
{
name: "favorite",
},
{
name: "add",
},
{
name: "delete",
},
{
name: "arrow_back",
},
{
name: "star",
},
{
name: "logout",
},
{
name: "add_circle",
},
{
name: "cancel",
},
{
name: "arrow_drop_down",
},
{
name: "more_vert",
},
{
name: "check",
},
{
name: "check_box",
},
{
name: "toggle_on",
},
{
name: "open_in_new",
},
{
name: "refresh",
},
{
name: "login",
},
{
name: "radio_button_unchecked",
},
{
name: "more_horiz",
},
{
name: "apps",
},
{
name: "radio_button_checked",
},
{
name: "download",
},
{
name: "remove",
},
{
name: "toggle_off",
},
{
name: "bolt",
},
{
name: "arrow_upward",
},
{
name: "filter_list",
},
{
name: "delete_forever",
},
{
name: "autorenew",
},
{
name: "key",
},
{
name: "sort",
},
{
name: "sync",
},
{
name: "add_box",
},
{
name: "block",
},
{
name: "restart_alt",
},
{
name: "menu_open",
},
{
name: "shopping_cart_checkout",
},
{
name: "expand_circle_down",
},
{
name: "backspace",
},
{
name: "undo",
},
{
name: "done_all",
},
{
name: "do_not_disturb_on",
},
{
name: "open_in_full",
},
{
name: "double_arrow",
},
{
name: "sync_alt",
},
{
name: "zoom_in",
},
{
name: "done_outline",
},
{
name: "drag_indicator",
},
{
name: "fullscreen",
},
{
name: "star_half",
},
{
name: "settings_accessibility",
},
{
name: "reply",
},
{
name: "exit_to_app",
},
{
name: "unfold_more",
},
{
name: "library_add",
},
{
name: "cached",
},
{
name: "select_check_box",
},
{
name: "terminal",
},
{
name: "change_circle",
},
{
name: "disabled_by_default",
},
{
name: "swap_horiz",
},
{
name: "swap_vert",
},
{
name: "app_registration",
},
{
name: "download_for_offline",
},
{
name: "close_fullscreen",
},
{
name: "file_open",
},
{
name: "minimize",
},
{
name: "open_with",
},
{
name: "dataset",
},
{
name: "add_task",
},
{
name: "start",
},
{
name: "keyboard_voice",
},
{
name: "create_new_folder",
},
{
name: "forward",
},
{
name: "download",
},
{
name: "settings_applications",
},
{
name: "compare_arrows",
},
{
name: "redo",
},
{
name: "zoom_out",
},
{
name: "publish",
},
{
name: "html",
},
{
name: "token",
},
{
name: "switch_access_shortcut",
},
{
name: "fullscreen_exit",
},
{
name: "sort_by_alpha",
},
{
name: "delete_sweep",
},
{
name: "indeterminate_check_box",
},
{
name: "view_timeline",
},
{
name: "settings_backup_restore",
},
{
name: "arrow_drop_down_circle",
},
{
name: "assistant_navigation",
},
{
name: "sync_problem",
},
{
name: "clear_all",
},
{
name: "density_medium",
},
{
name: "heart_plus",
},
{
name: "filter_alt_off",
},
{
name: "expand",
},
{
name: "subdirectory_arrow_right",
},
{
name: "download_done",
},
{
name: "arrow_outward",
},
{
name: "123",
},
{
name: "swipe_left",
},
{
name: "auto_mode",
},
{
name: "saved_search",
},
{
name: "place_item",
},
{
name: "system_update_alt",
},
{
name: "javascript",
},
{
name: "search_off",
},
{
name: "output",
},
{
name: "select_all",
},
{
name: "fit_screen",
},
{
name: "swipe_up",
},
{
name: "dynamic_form",
},
{
name: "hide_source",
},
{
name: "swipe_right",
},
{
name: "switch_access_shortcut_add",
},
{
name: "browse_gallery",
},
{
name: "css",
},
{
name: "density_small",
},
{
name: "assistant_direction",
},
{
name: "check_small",
},
{
name: "youtube_searched_for",
},
{
name: "move_up",
},
{
name: "swap_horizontal_circle",
},
{
name: "data_thresholding",
},
{
name: "install_mobile",
},
{
name: "move_down",
},
{
name: "dataset_linked",
},
{
name: "keyboard_command_key",
},
{
name: "view_kanban",
},
{
name: "swipe_down",
},
{
name: "key_off",
},
{
name: "transcribe",
},
{
name: "send_time_extension",
},
{
name: "swipe_down_alt",
},
{
name: "swipe_left_alt",
},
{
name: "swipe_right_alt",
},
{
name: "swipe_up_alt",
},
{
name: "keyboard_option_key",
},
{
name: "cycle",
},
{
name: "rebase",
},
{
name: "rebase_edit",
},
{
name: "empty_dashboard",
},
{
name: "magic_exchange",
},
{
name: "acute",
},
{
name: "point_scan",
},
{
name: "step_into",
},
{
name: "cheer",
},
{
name: "emoticon",
},
{
name: "explosion",
},
{
name: "water_bottle",
},
{
name: "weather_hail",
},
{
name: "syringe",
},
{
name: "pill",
},
{
name: "genetics",
},
{
name: "allergy",
},
{
name: "medical_mask",
},
{
name: "body_fat",
},
{
name: "barefoot",
},
{
name: "infrared",
},
{
name: "wrist",
},
{
name: "metabolism",
},
{
name: "conditions",
},
{
name: "taunt",
},
{
name: "altitude",
},
{
name: "tibia",
},
{
name: "footprint",
},
{
name: "eyeglasses",
},
{
name: "man_3",
},
{
name: "woman_2",
},
{
name: "rheumatology",
},
{
name: "tornado",
},
{
name: "landslide",
},
{
name: "foggy",
},
{
name: "severe_cold",
},
{
name: "tsunami",
},
{
name: "vape_free",
},
{
name: "sign_language",
},
{
name: "emoji_symbols",
},
{
name: "clear_night",
},
{
name: "emoji_food_beverage",
},
{
name: "hive",
},
{
name: "thunderstorm",
},
{
name: "communication",
},
{
name: "rocket",
},
{
name: "pets",
},
{
name: "public",
},
{
name: "quiz",
},
{
name: "mood",
},
{
name: "gavel",
},
{
name: "eco",
},
{
name: "diamond",
},
{
name: "forest",
},
{
name: "rainy",
},
{
name: "skull",
},
];
+1
View File
@@ -0,0 +1 @@
export * from "./emoji-icon-picker";
+16 -11
View File
@@ -1,4 +1,6 @@
import * as React from "react";
// helpers
import { cn } from "../../helpers";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
mode?: "primary" | "transparent" | "true-transparent";
@@ -16,17 +18,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
ref={ref}
type={type}
name={name}
className={`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${
mode === "primary"
? "rounded-md border-[0.5px] border-custom-border-200"
: mode === "transparent"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
: mode === "true-transparent"
? "rounded border-none bg-transparent ring-0"
: ""
} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${
inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""
} ${className}`}
className={cn(
`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${
mode === "primary"
? "rounded-md border-[0.5px] border-custom-border-200"
: mode === "transparent"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
: mode === "true-transparent"
? "rounded border-none bg-transparent ring-0"
: ""
} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${
inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""
}`,
className
)}
{...rest}
/>
);
+4 -1
View File
@@ -1,12 +1,15 @@
export * from "./avatar";
export * from "./breadcrumbs";
export * from "./badge";
export * from "./breadcrumbs";
export * from "./button";
export * from "./emoji";
export * from "./dropdowns";
export * from "./form-fields";
export * from "./icons";
export * from "./progress";
export * from "./spinners";
export * from "./tabs";
export * from "./tooltip";
export * from "./loader";
export * from "./control-link";
export * from "./toast";
@@ -0,0 +1,35 @@
import * as React from "react";
interface ICircularBarSpinner extends React.SVGAttributes<SVGElement> {
height?: string;
width?: string;
className?: string | undefined;
}
export const CircularBarSpinner: React.FC<ICircularBarSpinner> = ({
height = "16px",
width = "16px",
className = "",
}) => (
<div role="status">
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" className={className}>
<g>
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.14} />
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.29} transform="rotate(30 12 12)" />
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.43} transform="rotate(60 12 12)" />
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.57} transform="rotate(90 12 12)" />
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.71} transform="rotate(120 12 12)" />
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.86} transform="rotate(150 12 12)" />
<rect width={2} height={5} x={11} y={1} fill="currentColor" transform="rotate(180 12 12)" />
<animateTransform
attributeName="transform"
calcMode="discrete"
dur="0.75s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12"
/>
</g>
</svg>
</div>
);
+1
View File
@@ -1 +1,2 @@
export * from "./circular-spinner";
export * from "./circular-bar-spinner";
+84
View File
@@ -0,0 +1,84 @@
import React from "react";
// tooltip
import { Tooltip } from "../tooltip";
// helpers
import { cn } from "../../helpers";
export type TIconTabsProps = {
buttonClassName?: string;
containerClassName?: string;
hideTooltip?: boolean;
iconClassName?: string;
iconsList: {
key: string;
title: string;
icon: any;
}[];
onSelect: (key: string) => void;
overlayClassName?: string;
selectedKey: string | undefined;
};
export const IconTabs: React.FC<TIconTabsProps> = (props) => {
const {
buttonClassName,
containerClassName,
hideTooltip = false,
iconClassName,
iconsList,
onSelect,
overlayClassName,
selectedKey,
} = props;
const selectedTabIndex = iconsList.findIndex((icon) => icon.key === selectedKey);
return (
<div className={cn("relative flex items-center rounded-[5px] bg-custom-background-80 p-[2px]", containerClassName)}>
<div
className={cn(
"absolute z-0 bg-custom-background-100 top-1/2 rounded-[3px] transition-all duration-500 ease-in-out",
{
// right shadow
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== iconsList.length - 1,
// left shadow
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
},
overlayClassName
)}
style={{
height: "calc(100% - 4px)",
width: `calc((100% - 4px)/${iconsList.length})`,
transform: `translate(${selectedTabIndex * 100}%, -50%)`,
}}
/>
{iconsList.map((icon) => (
<Tooltip key={icon.key} tooltipContent={icon.title} disabled={hideTooltip}>
<button
type="button"
className={cn(
"relative grid h-[22px] w-7 place-items-center overflow-hidden rounded-[3px] transition-all z-[1] text-custom-text-200",
{
"text-custom-text-100": selectedKey == icon.key,
},
buttonClassName
)}
onClick={() => onSelect(icon.key)}
>
<icon.icon
size={14}
strokeWidth={2}
className={cn(
"h-3.5 w-3.5 text-custom-text-200",
{
"text-custom-text-100": selectedKey == icon.key,
},
iconClassName
)}
/>
</button>
</Tooltip>
))}
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./icon-tabs";
+206
View File
@@ -0,0 +1,206 @@
import * as React from "react";
import { Toaster, toast } from "sonner";
// icons
import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react";
// spinner
import { CircularBarSpinner } from "../spinners";
// helper
import { cn } from "../../helpers";
export enum TOAST_TYPE {
SUCCESS = "success",
ERROR = "error",
INFO = "info",
WARNING = "warning",
LOADING = "loading",
}
type SetToastProps =
| {
type: TOAST_TYPE.LOADING;
title?: string;
}
| {
id?: string | number;
type: Exclude<TOAST_TYPE, TOAST_TYPE.LOADING>;
title: string;
message?: string;
};
type PromiseToastCallback<ToastData> = (data: ToastData) => string;
type PromiseToastData<ToastData> = {
title: string;
message?: PromiseToastCallback<ToastData>;
};
type PromiseToastOptions<ToastData> = {
loading?: string;
success: PromiseToastData<ToastData>;
error: PromiseToastData<ToastData>;
};
type ToastContentProps = {
toastId: string | number;
icon?: React.ReactNode;
textColorClassName: string;
backgroundColorClassName: string;
borderColorClassName: string;
};
type ToastProps = {
theme: "light" | "dark" | "system";
};
export const Toast = (props: ToastProps) => {
const { theme } = props;
return <Toaster visibleToasts={5} gap={20} theme={theme} />;
};
export const setToast = (props: SetToastProps) => {
const renderToastContent = ({
toastId,
icon,
textColorClassName,
backgroundColorClassName,
borderColorClassName,
}: ToastContentProps) =>
props.type === TOAST_TYPE.LOADING ? (
<div
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className={cn("w-[350px] h-[67.3px] rounded-lg border shadow-sm p-2", backgroundColorClassName, borderColorClassName)}
>
<div className="w-full h-full flex items-center justify-center px-4 py-2">
{icon && <div className="flex items-center justify-center">{icon}</div>}
<div className={cn("w-full flex items-center gap-0.5 pr-1", icon ? "pl-4" : "pl-1")}>
<div className={cn("grow text-sm font-semibold", textColorClassName)}>{props.title ?? "Loading..."}</div>
<div className="flex-shrink-0">
<X
className="text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
strokeWidth={1.5}
width={14}
height={14}
onClick={() => toast.dismiss(toastId)}
/>
</div>
</div>
</div>
</div>
) : (
<div
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className={cn(
"relative flex flex-col w-[350px] rounded-lg border shadow-sm p-2",
backgroundColorClassName,
borderColorClassName
)}
>
<X
className="fixed top-2 right-2.5 text-toast-text-secondary hover:text-toast-text-tertiary cursor-pointer"
strokeWidth={1.5}
width={14}
height={14}
onClick={() => toast.dismiss(toastId)}
/>
<div className="w-full flex items-center px-4 py-2">
{icon && <div className="flex items-center justify-center">{icon}</div>}
<div className={cn("flex flex-col gap-0.5 pr-1", icon ? "pl-6" : "pl-1")}>
<div className={cn("text-sm font-semibold", textColorClassName)}>{props.title}</div>
{props.message && <div className="text-toast-text-secondary text-xs font-medium">{props.message}</div>}
</div>
</div>
</div>
);
switch (props.type) {
case TOAST_TYPE.SUCCESS:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <CheckCircle2 width={28} height={28} strokeWidth={1.5} className="text-toast-text-success" />,
textColorClassName: "text-toast-text-success",
backgroundColorClassName: "bg-toast-background-success",
borderColorClassName: "border-toast-border-success",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.ERROR:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <XCircle width={28} height={28} strokeWidth={1.5} className="text-toast-text-error" />,
textColorClassName: "text-toast-text-error",
backgroundColorClassName: "bg-toast-background-error",
borderColorClassName: "border-toast-border-error",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.WARNING:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
icon: <AlertTriangle width={28} height={28} strokeWidth={1.5} className="text-toast-text-warning" />,
textColorClassName: "text-toast-text-warning",
backgroundColorClassName: "bg-toast-background-warning",
borderColorClassName: "border-toast-border-warning",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.INFO:
return toast.custom(
(toastId) =>
renderToastContent({
toastId,
textColorClassName: "text-toast-text-info",
backgroundColorClassName: "bg-toast-background-info",
borderColorClassName: "border-toast-border-info",
}),
props.id ? { id: props.id } : {}
);
case TOAST_TYPE.LOADING:
return toast.custom((toastId) =>
renderToastContent({
toastId,
icon: <CircularBarSpinner className="text-toast-text-tertiary" />,
textColorClassName: "text-toast-text-loading",
backgroundColorClassName: "bg-toast-background-loading",
borderColorClassName: "border-toast-border-loading",
})
);
}
};
export const setPromiseToast = <ToastData,>(
promise: Promise<ToastData>,
options: PromiseToastOptions<ToastData>
): void => {
const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading });
promise
.then((data: ToastData) => {
setToast({
type: TOAST_TYPE.SUCCESS,
id: tId,
title: options.success.title,
message: options.success.message?.(data),
});
})
.catch((data: ToastData) => {
setToast({
type: TOAST_TYPE.ERROR,
id: tId,
title: options.error.title,
message: options.error.message?.(data),
});
});
};
+1
View File
@@ -1 +1,2 @@
export * from "./latest-feature-block";
export * from "./project-logo";
+34
View File
@@ -0,0 +1,34 @@
// helpers
import { cn } from "helpers/common.helper";
// types
import { TProjectLogoProps } from "@plane/types";
type Props = {
className?: string;
logo: TProjectLogoProps;
};
export const ProjectLogo: React.FC<Props> = (props) => {
const { className, logo } = props;
if (logo.in_use === "icon" && logo.icon)
return (
<span
style={{
color: logo.icon.color,
}}
className={cn("material-symbols-rounded text-base", className)}
>
{logo.icon.name}
</span>
);
if (logo.in_use === "emoji" && logo.emoji)
return (
<span className={cn("text-base", className)}>
{logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))}
</span>
);
return <span />;
};
+10 -37
View File
@@ -1,15 +1,12 @@
import { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
// components
// import { NavbarSearch } from "./search";
import { NavbarIssueBoardView } from "./issue-board-view";
import { NavbarTheme } from "./theme";
import { IssueFiltersDropdown } from "components/issues/filters";
import { ProjectLogo } from "components/common";
// ui
import { Avatar, Button } from "@plane/ui";
import { Briefcase } from "lucide-react";
@@ -19,18 +16,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { TIssueBoardKeys } from "types/issue";
const renderEmoji = (emoji: string | { name: string; color: string }) => {
if (!emoji) return;
if (typeof emoji === "object")
return (
<span style={{ color: emoji.color }} className="material-symbols-rounded text-lg">
{emoji.name}
</span>
);
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};
const IssueNavbar = observer(() => {
const {
project: projectStore,
@@ -123,27 +108,15 @@ const IssueNavbar = observer(() => {
<div className="relative flex w-full items-center gap-4 px-5">
{/* project detail */}
<div className="flex flex-shrink-0 items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center">
{projectStore.project ? (
projectStore.project?.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(projectStore.project.emoji)}
</span>
) : projectStore.project?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(projectStore.project.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectStore.project?.name.charAt(0)}
</span>
)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)}
</div>
{projectStore.project ? (
<span className="h-7 w-7 flex-shrink-0 grid place-items-center">
<ProjectLogo logo={projectStore.project.logo_props} className="text-lg" />
</span>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)}
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
{projectStore?.project?.name || `...`}
</div>
+3 -3
View File
@@ -67,13 +67,13 @@ const DropdownList: React.FC<DropDownListProps> = (props) => {
const DropdownItem: React.FC<DropdownItemProps> = (props) => {
const { item } = props;
const { display, children, as: as_, href, onClick, isSelected } = item;
const { display, children, as: itemAs, href, onClick, isSelected } = item;
const [open, setOpen] = useState(false);
return (
<div className="group relative flex w-full gap-x-6 rounded-lg p-1">
{(!as_ || as_ === "button" || as_ === "div") && (
{(!itemAs || itemAs === "button" || itemAs === "div") && (
<button
type="button"
onClick={() => {
@@ -94,7 +94,7 @@ const DropdownItem: React.FC<DropdownItemProps> = (props) => {
</button>
)}
{as_ === "link" && <Link href={href || "#"}>{display}</Link>}
{itemAs === "link" && <Link href={href || "#"}>{display}</Link>}
{children && <DropdownList open={open} handleClose={() => setOpen(false)} items={children} />}
</div>
+4 -4
View File
@@ -9,10 +9,10 @@ let rootStore: RootStore = new RootStore();
export const MobxStoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const _rootStore: RootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return _rootStore;
if (!rootStore) rootStore = _rootStore;
return _rootStore;
const singletonRootStore: RootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return singletonRootStore;
if (!rootStore) rootStore = singletonRootStore;
return singletonRootStore;
};
export const MobxStoreProvider = ({ children }: any) => {
-2
View File
@@ -49,9 +49,7 @@
"@types/react-dom": "^18.2.17",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"eslint": "8.34.0",
"eslint-config-custom": "*",
"eslint-config-next": "13.2.1",
"tailwind-config-custom": "*",
"tsconfig": "*"
}
+3 -3
View File
@@ -1,3 +1,5 @@
import { TProjectLogoProps } from "@plane/types";
export interface IWorkspace {
id: string;
name: string;
@@ -9,10 +11,8 @@ export interface IProject {
identifier: string;
name: string;
description: string;
icon: string;
cover_image: string | null;
icon_prop: string | null;
emoji: string | null;
logo_props: TProjectLogoProps;
}
export interface IProjectSettings {
+1
View File
@@ -16,6 +16,7 @@
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST"
],
+99
View File
@@ -1,4 +1,103 @@
module.exports = {
root: true,
extends: ["custom"],
parser: "@typescript-eslint/parser",
settings: {
"import/resolver": {
typescript: {},
node: {
moduleDirectory: ["node_modules", "."],
},
},
},
rules: {
// "import/order": [
// "error",
// {
// groups: ["builtin", "external", "internal", "parent", "sibling"],
// pathGroups: [
// {
// pattern: "react",
// group: "external",
// position: "before",
// },
// {
// pattern: "@headlessui/**",
// group: "external",
// position: "after",
// },
// {
// pattern: "lucide-react",
// group: "external",
// position: "after",
// },
// {
// pattern: "@plane/ui",
// group: "external",
// position: "after",
// },
// {
// pattern: "components/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "constants/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "contexts/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "helpers/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "hooks/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "layouts/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "lib/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "services/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "store/**",
// group: "internal",
// position: "before",
// },
// {
// pattern: "@plane/types",
// group: "internal",
// position: "after",
// },
// {
// pattern: "lib/types",
// group: "internal",
// position: "after",
// },
// ],
// pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
// alphabetize: {
// order: "asc",
// caseInsensitive: true,
// },
// },
// ],
},
};
@@ -1,15 +1,13 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
import { Trash2 } from "lucide-react";
import { mutate } from "swr";
// hooks
import { useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { useUser } from "hooks/store";
type Props = {
isOpen: boolean;
@@ -26,7 +24,6 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
const router = useRouter();
const { setToastAlert } = useToast();
const { setTheme } = useTheme();
const handleClose = () => {
@@ -39,8 +36,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
await deactivateAccount()
.then(() => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Account deactivated successfully.",
});
@@ -50,8 +47,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
handleClose();
})
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error,
})
@@ -89,8 +86,11 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="">
<div className="flex items-start gap-x-4">
<div className="grid place-items-center rounded-full bg-red-500/20 p-2 sm:p-2 md:p-4 lg:p-4 mt-3 sm:mt-3 md:mt-0 lg:mt-0 ">
<Trash2 className="h-4 w-4 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6 text-red-600" aria-hidden="true" />
<div className="mt-3 grid place-items-center rounded-full bg-red-500/20 p-2 sm:mt-3 sm:p-2 md:mt-0 md:p-4 lg:mt-0 lg:p-4 ">
<Trash2
className="h-4 w-4 text-red-600 sm:h-4 sm:w-4 md:h-6 md:w-6 lg:h-6 lg:w-6"
aria-hidden="true"
/>
</div>
<div>
<Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100">
@@ -1,11 +1,10 @@
import { observer } from "mobx-react-lite";
// services
import { AuthService } from "services/auth.service";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
import { useApplication } from "hooks/store";
// ui
// components
type Props = {
handleSignInRedirection: () => Promise<void>;
@@ -17,8 +16,6 @@ const authService = new AuthService();
export const OAuthOptions: React.FC<Props> = observer((props) => {
const { handleSignInRedirection, type } = props;
// toast alert
const { setToastAlert } = useToast();
// mobx store
const {
config: { envConfig },
@@ -39,9 +36,9 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
if (response) handleSignInRedirection();
} else throw Error("Cant find credentials");
} catch (err: any) {
setToastAlert({
setToast({
type: TOAST_TYPE.ERROR,
title: "Error signing in!",
type: "error",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
@@ -60,9 +57,9 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
if (response) handleSignInRedirection();
} else throw Error("Cant find credentials");
} catch (err: any) {
setToastAlert({
setToast({
type: TOAST_TYPE.ERROR,
title: "Error signing in!",
type: "error",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
@@ -4,10 +4,8 @@ import { XCircle } from "lucide-react";
import { observer } from "mobx-react-lite";
// services
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
@@ -27,7 +25,6 @@ const authService = new AuthService();
export const SignInEmailForm: React.FC<Props> = observer((props) => {
const { onSubmit, updateEmail } = props;
// hooks
const { setToastAlert } = useToast();
const {
control,
formState: { errors, isSubmitting, isValid },
@@ -52,8 +49,8 @@ export const SignInEmailForm: React.FC<Props> = observer((props) => {
.emailCheck(payload)
.then((res) => onSubmit(res.is_password_autoset))
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -3,10 +3,9 @@ import { Controller, useForm } from "react-hook-form";
// services
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// icons
@@ -38,8 +37,6 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// form info
const {
control,
@@ -62,8 +59,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
await authService
.setPassword(payload)
.then(async () => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Password created successfully.",
});
@@ -78,8 +75,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
state: "FAILED",
first_time: false,
});
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
@@ -160,7 +157,7 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
Whatever you choose now will be your account{"'"}s password until you change it.
</p>
</div>
@@ -1,23 +1,22 @@
import React, { useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff, XCircle } from "lucide-react";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
import { checkEmailValidity } from "helpers/string.helper";
import { useApplication, useEventTracker } from "hooks/store";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
import { useApplication, useEventTracker } from "hooks/store";
// components
import { ESignInSteps, ForgotPasswordPopover } from "components/account";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IPasswordSignInData } from "@plane/types";
// constants
import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker";
type Props = {
email: string;
@@ -43,8 +42,6 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
// states
const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
const {
config: { envConfig },
} = useApplication();
@@ -83,8 +80,8 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
await onSubmit();
})
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -107,8 +104,8 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
.generateUniqueCode({ email: emailFormValue })
.then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD))
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -1,11 +1,7 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// hooks
import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import { LatestFeatureBlock } from "components/common";
import {
SignInEmailForm,
SignInUniqueCodeForm,
@@ -13,8 +9,12 @@ import {
OAuthOptions,
SignInOptionalSetPasswordForm,
} from "components/account";
// constants
import { LatestFeatureBlock } from "components/common";
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
// constants
export enum ESignInSteps {
EMAIL = "EMAIL",
@@ -2,20 +2,21 @@ import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { CODE_VERIFIED } from "constants/event-tracker";
import { checkEmailValidity } from "helpers/string.helper";
import { useEventTracker } from "hooks/store";
import useTimer from "hooks/use-timer";
import { AuthService } from "services/auth.service";
import { UserService } from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer";
import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// constants
import { CODE_VERIFIED } from "constants/event-tracker";
type Props = {
email: string;
@@ -42,8 +43,6 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
const { email, onSubmit, handleEmailClear, submitButtonText } = props;
// states
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// store hooks
const { captureEvent } = useEventTracker();
// timer
@@ -84,8 +83,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
@@ -101,8 +100,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
.generateUniqueCode(payload)
.then(() => {
setResendCodeTimer(30);
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "A new unique code has been sent to your email.",
});
@@ -113,8 +112,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
});
})
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
+7 -10
View File
@@ -1,15 +1,13 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
import { observer } from "mobx-react-lite";
// services
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { checkEmailValidity } from "helpers/string.helper";
import { AuthService } from "services/auth.service";
// ui
// helpers
// types
import { IEmailCheckData } from "@plane/types";
@@ -27,7 +25,6 @@ const authService = new AuthService();
export const SignUpEmailForm: React.FC<Props> = observer((props) => {
const { onSubmit, updateEmail } = props;
// hooks
const { setToastAlert } = useToast();
const {
control,
formState: { errors, isSubmitting, isValid },
@@ -52,8 +49,8 @@ export const SignUpEmailForm: React.FC<Props> = observer((props) => {
.emailCheck(payload)
.then(() => onSubmit())
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -1,19 +1,19 @@
import React, { useState } from "react";
import { Controller, useForm } from "react-hook-form";
// services
import { Eye, EyeOff } from "lucide-react";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { ESignUpSteps } from "components/account";
import { PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
import { checkEmailValidity } from "helpers/string.helper";
import { useEventTracker } from "hooks/store";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// components
// constants
import { ESignUpSteps } from "components/account";
import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker";
// icons
import { Eye, EyeOff } from "lucide-react";
type Props = {
email: string;
@@ -41,8 +41,6 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// form info
const {
control,
@@ -65,8 +63,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
await authService
.setPassword(payload)
.then(async () => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Password created successfully.",
});
@@ -81,8 +79,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
state: "FAILED",
first_time: true,
});
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
@@ -164,7 +162,7 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
This password will continue to be your account{"'"}s password.
</p>
</div>
@@ -1,16 +1,14 @@
import React, { useState } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff, XCircle } from "lucide-react";
// services
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
import { AuthService } from "services/auth.service";
// types
import { IPasswordSignInData } from "@plane/types";
@@ -34,8 +32,6 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
const { onSubmit } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// form info
const {
control,
@@ -59,8 +55,8 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
.passwordSignIn(payload)
.then(async () => await onSubmit())
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -138,7 +134,7 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
</div>
)}
/>
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
This password will continue to be your account{"'"}s password.
</p>
</div>
@@ -1,9 +1,7 @@
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import Link from "next/link";
import {
OAuthOptions,
SignUpEmailForm,
@@ -11,9 +9,11 @@ import {
SignUpPasswordForm,
SignUpUniqueCodeForm,
} from "components/account";
import Link from "next/link";
// constants
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
import { useApplication, useEventTracker } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
// constants
export enum ESignUpSteps {
EMAIL = "EMAIL",
@@ -3,20 +3,20 @@ import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
// services
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { CODE_VERIFIED } from "constants/event-tracker";
import { checkEmailValidity } from "helpers/string.helper";
import { useEventTracker } from "hooks/store";
import useTimer from "hooks/use-timer";
import { AuthService } from "services/auth.service";
import { UserService } from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer";
import { useEventTracker } from "hooks/store";
// ui
import { Button, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "helpers/string.helper";
// types
import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// constants
import { CODE_VERIFIED } from "constants/event-tracker";
type Props = {
email: string;
@@ -44,8 +44,6 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
// store hooks
const { captureEvent } = useEventTracker();
// toast alert
const { setToastAlert } = useToast();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30);
// form info
@@ -84,8 +82,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
captureEvent(CODE_VERIFIED, {
state: "FAILED",
});
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
});
@@ -101,8 +99,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
.generateUniqueCode(payload)
.then(() => {
setResendCodeTimer(30);
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "A new unique code has been sent to your email.",
});
@@ -112,8 +110,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
});
})
.catch((err) =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error ?? "Something went wrong. Please try again.",
})
@@ -1,17 +1,17 @@
import { useRouter } from "next/router";
import useSWR from "swr";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import useSWR from "swr";
// services
import { AnalyticsService } from "services/analytics.service";
// components
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
// types
import { IAnalyticsParams } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
import { useApplication } from "hooks/store";
import { AnalyticsService } from "services/analytics.service";
import { IAnalyticsParams } from "@plane/types";
type Props = {
additionalParams?: Partial<IAnalyticsParams>;
@@ -60,8 +60,8 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
? "capitalize"
: ""
: params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize"
: ""
? "capitalize"
: ""
}`}
>
{params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}:
@@ -1,15 +1,15 @@
// nivo
import { BarDatum } from "@nivo/bar";
// components
import { CustomTooltip } from "./custom-tooltip";
import { Tooltip } from "@plane/ui";
// ui
import { BarGraph } from "components/ui";
// helpers
import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
import { findStringWithMostCharacters } from "helpers/array.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
import { CustomTooltip } from "./custom-tooltip";
type Props = {
analytics: IAnalyticsResponse;
@@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, param
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
: "?"
: datum.value && datum.value !== "None"
? `${datum.value}`.toUpperCase()[0]
: "?"}
? `${datum.value}`.toUpperCase()[0]
: "?"}
</text>
</g>
</Tooltip>
@@ -2,15 +2,15 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// components
import { Button, Loader } from "@plane/ui";
import { AnalyticsGraph, AnalyticsTable } from "components/analytics";
// ui
import { Button, Loader } from "@plane/ui";
// helpers
import { ANALYTICS } from "constants/fetch-keys";
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
type Props = {
analytics: IAnalyticsResponse | undefined;
@@ -33,7 +33,7 @@ export const CustomAnalyticsMainContent: React.FC<Props> = (props) => {
{!error ? (
analytics ? (
analytics.total > 0 ? (
<div className="h-full overflow-y-auto">
<div className="h-full overflow-y-auto vertical-scrollbar scrollbar-md">
<AnalyticsGraph
analytics={analytics}
barGraphData={barGraphData}
@@ -1,9 +1,9 @@
import { observer } from "mobx-react-lite";
import { Control, Controller, UseFormSetValue } from "react-hook-form";
// hooks
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
import { useProject } from "hooks/store";
// components
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
// types
import { IAnalyticsParams } from "@plane/types";
@@ -22,8 +22,9 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
return (
<div
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""
}`}
className={`grid items-center gap-4 px-5 py-2.5 ${
isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"
} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""}`}
>
{!isProjectLevel && (
<div>
@@ -1,8 +1,8 @@
import { observer } from "mobx-react-lite";
// hooks
import { CustomSearchSelect } from "@plane/ui";
import { useProject } from "hooks/store";
// ui
import { CustomSearchSelect } from "@plane/ui";
type Props = {
value: string[] | undefined;
@@ -3,9 +3,9 @@ import { useRouter } from "next/router";
// ui
import { CustomSelect } from "@plane/ui";
// types
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
import { IAnalyticsParams, TXAxisValues } from "@plane/types";
// constants
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
type Props = {
value: TXAxisValues | null | undefined;
@@ -3,9 +3,9 @@ import { useRouter } from "next/router";
// ui
import { CustomSelect } from "@plane/ui";
// types
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
import { IAnalyticsParams, TXAxisValues } from "@plane/types";
// constants
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
type Props = {
value: TXAxisValues;
@@ -1,9 +1,9 @@
// ui
import { CustomSelect } from "@plane/ui";
// types
import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
import { TYAxisValues } from "@plane/types";
// constants
import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
type Props = {
value: TYAxisValues;
@@ -1,11 +1,11 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// icons
import { Contrast, LayoutGrid, Users } from "lucide-react";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
import { useProject } from "hooks/store";
import { ProjectLogo } from "components/project";
type Props = {
projectIds: string[];
@@ -19,7 +19,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
return (
<div className="relative flex flex-col gap-4 h-full">
<h4 className="font-medium">Selected Projects</h4>
<div className="relative space-y-6 overflow-hidden overflow-y-auto">
<div className="relative space-y-6 overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md">
{projectIds.map((projectId) => {
const project = getProjectById(projectId);
@@ -28,21 +28,15 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
return (
<div key={projectId} className="w-full">
<div className="flex items-center gap-1 text-sm">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
) : project.icon_prop ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<div className="h-6 w-6 grid place-items-center">
<ProjectLogo logo={project.logo_props} />
</div>
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>
</h5>
</div>
<div className="mt-4 w-full space-y-3 pl-2">
<div className="mt-4 w-full space-y-3 px-2">
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
@@ -1,12 +1,13 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { useCycle, useMember, useModule, useProject } from "hooks/store";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
// constants
import { NETWORK_CHOICES } from "constants/project";
import { renderFormattedDate } from "helpers/date-time.helper";
import { useCycle, useMember, useModule, useProject } from "hooks/store";
// components
import { ProjectLogo } from "components/project";
// helpers
// constants
export const CustomAnalyticsSidebarHeader = observer(() => {
const router = useRouter();
@@ -81,15 +82,9 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
) : (
<div className="h-full overflow-y-auto">
<div className="flex items-center gap-1">
{projectDetails?.emoji ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
) : projectDetails?.icon_prop ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
{renderEmoji(projectDetails.icon_prop)}
</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectDetails?.name.charAt(0)}
{projectDetails && (
<span className="h-6 w-6 grid place-items-center flex-shrink-0">
<ProjectLogo logo={projectDetails.logo_props} />
</span>
)}
<h4 className="break-words font-medium">{projectDetails?.name}</h4>
@@ -1,25 +1,24 @@
import { useEffect, } from "react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import { AnalyticsService } from "services/analytics.service";
// hooks
import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
// ui
import { Button, LayersIcon } from "@plane/ui";
// icons
import { CalendarDays, Download, RefreshCw } from "lucide-react";
import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
// icons
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store";
import { AnalyticsService } from "services/analytics.service";
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
type Props = {
analytics: IAnalyticsResponse | undefined;
@@ -34,8 +33,6 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
// toast alert
const { setToastAlert } = useToast();
// store hooks
const { currentUser } = useUser();
const { workspaceProjectIds, getProjectById } = useProject();
@@ -107,8 +104,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
analyticsService
.exportAnalytics(workspaceSlug.toString(), data)
.then((res) => {
setToastAlert({
type: "success",
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: res.message,
});
@@ -116,8 +113,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
trackExportAnalytics();
})
.catch(() =>
setToastAlert({
type: "error",
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "There was some error in exporting the analytics. Please try again.",
})
@@ -146,7 +143,7 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
return (
<div
className={cn(
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100",
"relative flex h-full w-full items-start justify-between gap-2 bg-custom-sidebar-background-100 px-5 py-4",
!isProjectLevel ? "flex-col" : ""
)}
>
@@ -179,10 +176,10 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
</>
</div>
<div className="flex flex-wrap items-center gap-2 justify-end">
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3 md:h-3.5 w-3 md:w-3.5" />}
prependIcon={<RefreshCw className="h-3 w-3 md:h-3.5 md:w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
@@ -3,11 +3,11 @@ import { BarDatum } from "@nivo/bar";
// icons
import { PriorityIcon } from "@plane/ui";
// helpers
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "@plane/types";
// constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
type Props = {
analytics: IAnalyticsResponse;

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