Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8a5095f14 | ||
|
|
1a462711e1 | ||
|
|
aa3702cd46 | ||
|
|
aed87ef472 | ||
|
|
aaad37575b | ||
|
|
42b524b16a | ||
|
|
0bc4b6cece | ||
|
|
43c75f4457 | ||
|
|
9c13dbd957 | ||
|
|
8d9adf4d87 | ||
|
|
4ccb505f36 | ||
|
|
884856b021 | ||
|
|
552c66457a | ||
|
|
1363ef0b2d | ||
|
|
898cf98be3 | ||
|
|
6ec9c64f7c | ||
|
|
f493a03f56 | ||
|
|
cb78ccad1f | ||
|
|
6c6b7156bb | ||
|
|
8cc372679c | ||
|
|
94327b8311 |
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Build-Push Web/Space/API/Proxy Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- nginx/**
|
||||
|
||||
branch_build_push_frontend:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_space:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_backend:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_proxy:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
@@ -280,4 +280,3 @@ jobs:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'develop', 'preview', 'master' ]
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ 'develop', 'preview', 'master' ]
|
||||
branches: ["develop", "preview", "master"]
|
||||
schedule:
|
||||
- cron: '53 19 * * 5'
|
||||
- cron: "53 19 * * 5"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -21,45 +20,44 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python', 'javascript' ]
|
||||
language: ["python", "javascript"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -4,70 +4,196 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
web-build:
|
||||
required: true
|
||||
required: false
|
||||
description: 'Build Web'
|
||||
type: boolean
|
||||
default: true
|
||||
space-build:
|
||||
required: true
|
||||
required: false
|
||||
description: 'Build Space'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
BUILD_WEB: ${{ github.event.inputs.web-build }}
|
||||
BUILD_SPACE: ${{ github.event.inputs.space-build }}
|
||||
|
||||
jobs:
|
||||
setup-feature-build:
|
||||
name: Feature Build Setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
echo "BUILD_WEB=$BUILD_WEB"
|
||||
echo "BUILD_SPACE=$BUILD_SPACE"
|
||||
outputs:
|
||||
web-build: ${{ env.BUILD_WEB}}
|
||||
space-build: ${{env.BUILD_SPACE}}
|
||||
|
||||
feature-build-web:
|
||||
if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }}
|
||||
needs: setup-feature-build
|
||||
name: Feature Build Web
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
|
||||
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install awscli
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: plane
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/plane
|
||||
yarn install
|
||||
- name: Build Web
|
||||
id: build-web
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/plane
|
||||
yarn build --filter=web
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
TAR_NAME="web.tar.gz"
|
||||
tar -czf $TAR_NAME ./plane
|
||||
|
||||
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
|
||||
|
||||
feature-build-space:
|
||||
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
|
||||
needs: setup-feature-build
|
||||
name: Feature Build Space
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
||||
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
|
||||
outputs:
|
||||
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
|
||||
s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }}
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install awscli
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: plane
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/plane
|
||||
yarn install
|
||||
- name: Build Space
|
||||
id: build-space
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/plane
|
||||
yarn build --filter=space
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
TAR_NAME="space.tar.gz"
|
||||
tar -czf $TAR_NAME ./plane
|
||||
|
||||
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
|
||||
|
||||
feature-deploy:
|
||||
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }}
|
||||
needs: [feature-build-web, feature-build-space]
|
||||
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 }}
|
||||
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
|
||||
KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }}
|
||||
steps:
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install awscli
|
||||
- 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"
|
||||
curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_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 }}
|
||||
WEB_S3_URL=""
|
||||
if [ ${{ env.BUILD_WEB }} == true ]; then
|
||||
WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600)
|
||||
fi
|
||||
|
||||
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)
|
||||
SPACE_S3_URL=""
|
||||
if [ ${{ env.BUILD_SPACE }} == true ]; then
|
||||
SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600)
|
||||
fi
|
||||
|
||||
APP_NAME=$(echo $METADATA | jq -r '.name')
|
||||
if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then
|
||||
|
||||
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')
|
||||
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
|
||||
|
||||
echo "****************************************"
|
||||
echo "APP NAME ::: $APP_NAME"
|
||||
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
|
||||
echo "****************************************"
|
||||
APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}"
|
||||
DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}"
|
||||
|
||||
METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \
|
||||
--generate-name \
|
||||
--namespace $APP_NAMESPACE \
|
||||
--set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \
|
||||
--set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
|
||||
--set web.enabled=${{ env.BUILD_WEB || false }} \
|
||||
--set web.artifact_url=$WEB_S3_URL \
|
||||
--set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
|
||||
--set space.enabled=${{ env.BUILD_SPACE || false }} \
|
||||
--set space.artifact_url=$SPACE_S3_URL \
|
||||
--set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \
|
||||
--set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \
|
||||
--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 "****************************************"
|
||||
fi
|
||||
|
||||
@@ -50,6 +50,7 @@ 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.
|
||||
|
||||
@@ -53,6 +53,7 @@ NGINX_PORT=80
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
```
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/apiserver/.env
|
||||
|
||||
|
||||
|
||||
@@ -44,18 +44,20 @@ 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 | [](https://docs.plane.so/docker-compose) |
|
||||
| Kubernetes | [](https://docs.plane.so/kubernetes) |
|
||||
| Installation Methods | Documentation Link |
|
||||
|-----------------|----------------------------------------------------------------------------------------------------------|
|
||||
| Docker | [](https://docs.plane.so/docker-compose) |
|
||||
| Kubernetes | [](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
|
||||
|
||||
@@ -72,7 +74,9 @@ 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
|
||||
|
||||
@@ -97,10 +101,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
|
||||
```
|
||||
|
||||
@@ -115,7 +119,6 @@ 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
|
||||
|
||||

|
||||
|
||||
## 📸 Screenshots
|
||||
@@ -178,21 +181,20 @@ 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>
|
||||
@@ -1,9 +1,8 @@
|
||||
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
|
||||
@@ -285,20 +284,6 @@ 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(
|
||||
@@ -310,17 +295,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 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
|
||||
@@ -434,20 +432,6 @@ 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(
|
||||
@@ -459,17 +443,6 @@ 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):
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
"identifier",
|
||||
"name",
|
||||
"cover_image",
|
||||
"logo_props",
|
||||
"icon_prop",
|
||||
"emoji",
|
||||
"description",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -22,7 +22,6 @@ from plane.app.views import (
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
WorkspaceModulesEndpoint,
|
||||
WorkspaceCyclesEndpoint,
|
||||
)
|
||||
@@ -192,11 +191,6 @@ 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(),
|
||||
|
||||
@@ -49,7 +49,6 @@ from .workspace import (
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
ExportWorkspaceUserActivityEndpoint,
|
||||
WorkspaceModulesEndpoint,
|
||||
WorkspaceCyclesEndpoint,
|
||||
)
|
||||
@@ -187,6 +186,4 @@ from .webhook import (
|
||||
from .dashboard import (
|
||||
DashboardEndpoint,
|
||||
WidgetsEndpoint
|
||||
)
|
||||
|
||||
from .error_404 import custom_404_view
|
||||
)
|
||||
@@ -14,7 +14,6 @@ from django.db.models import (
|
||||
JSONField,
|
||||
Func,
|
||||
Prefetch,
|
||||
IntegerField,
|
||||
)
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -39,8 +38,6 @@ from plane.db.models import (
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
IssueRelation,
|
||||
IssueAssignee,
|
||||
User,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
@@ -215,11 +212,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(
|
||||
{
|
||||
@@ -234,11 +231,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(
|
||||
{
|
||||
@@ -368,11 +365,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(
|
||||
{
|
||||
@@ -385,11 +382,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(
|
||||
{
|
||||
@@ -506,9 +503,7 @@ 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],
|
||||
@@ -517,97 +512,90 @@ def dashboard_recent_projects(self, request, slug):
|
||||
|
||||
|
||||
def dashboard_recent_collaborators(self, request, slug):
|
||||
# Subquery to count activities for each project member
|
||||
activity_count_subquery = (
|
||||
# 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 = (
|
||||
IssueActivity.objects.filter(
|
||||
workspace__slug=slug,
|
||||
actor=OuterRef("member"),
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project_id__in=user_projects,
|
||||
)
|
||||
.values("actor")
|
||||
.annotate(num_activities=Count("pk"))
|
||||
.values("num_activities")
|
||||
)
|
||||
.exclude(actor=request.user)
|
||||
.annotate(num_activities=Count("actor"))
|
||||
.order_by("-num_activities")
|
||||
)[:7]
|
||||
|
||||
# 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,
|
||||
# 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}
|
||||
)
|
||||
.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)
|
||||
|
||||
# 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)
|
||||
)
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=project_members_with_activities,
|
||||
controller=self.get_results_controller,
|
||||
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},
|
||||
)
|
||||
|
||||
return Response(users_with_active_issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
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():
|
||||
@@ -634,9 +622,7 @@ 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:
|
||||
@@ -653,9 +639,7 @@ 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(
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# views.py
|
||||
from django.http import JsonResponse
|
||||
|
||||
def custom_404_view(request, exception=None):
|
||||
return JsonResponse({"error": "Page not found."}, status=404)
|
||||
@@ -88,7 +88,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
Q(
|
||||
project_projectmember__member=self.request.user,
|
||||
project_projectmember__is_active=True,
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
@@ -173,10 +176,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
projects = self.get_queryset().order_by("sort_order", "name")
|
||||
if request.GET.get("per_page", False) and request.GET.get(
|
||||
"cursor", False
|
||||
):
|
||||
@@ -576,9 +576,11 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
_ = WorkspaceMember.objects.create(
|
||||
workspace_id=project_invite.workspace_id,
|
||||
member=user,
|
||||
role=15
|
||||
if project_invite.role >= 15
|
||||
else project_invite.role,
|
||||
role=(
|
||||
15
|
||||
if project_invite.role >= 15
|
||||
else project_invite.role
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Else make him active
|
||||
@@ -685,9 +687,14 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
bulk_project_members = []
|
||||
member_roles = {member.get("member_id"): member.get("role") for member in members}
|
||||
member_roles = {
|
||||
member.get("member_id"): member.get("role") for member in members
|
||||
}
|
||||
# Update roles in the members array based on the member_roles dictionary
|
||||
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
|
||||
for project_member in ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
):
|
||||
project_member.role = member_roles[str(project_member.member_id)]
|
||||
project_member.is_active = True
|
||||
bulk_project_members.append(project_member)
|
||||
@@ -710,9 +717,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
role=member.get("role", 10),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
sort_order=sort_order[0] - 10000
|
||||
if len(sort_order)
|
||||
else 65535,
|
||||
sort_order=(
|
||||
sort_order[0] - 10000 if len(sort_order) else 65535
|
||||
),
|
||||
)
|
||||
)
|
||||
bulk_issue_props.append(
|
||||
@@ -733,7 +740,10 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
)
|
||||
serializer = ProjectMemberRoleSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# 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
|
||||
@@ -1241,66 +1238,6 @@ 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)
|
||||
@@ -1366,6 +1303,10 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"identifier",
|
||||
"emoji",
|
||||
"icon_prop",
|
||||
"created_issues",
|
||||
"assigned_issues",
|
||||
"completed_issues",
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# 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),
|
||||
]
|
||||
@@ -320,7 +320,7 @@ class IssueAssignee(ProjectBaseModel):
|
||||
|
||||
class IssueLink(ProjectBaseModel):
|
||||
title = models.CharField(max_length=255, null=True, blank=True)
|
||||
url = models.TextField()
|
||||
url = models.URLField()
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_link"
|
||||
)
|
||||
|
||||
@@ -107,7 +107,6 @@ 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,
|
||||
|
||||
@@ -7,7 +7,6 @@ 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")),
|
||||
|
||||
+50
-53
@@ -1,82 +1,79 @@
|
||||
# 1-Click Self-Hosting
|
||||
# One-click deploy
|
||||
|
||||
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
|
||||
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
|
||||
|
||||
Let's get started!
|
||||
This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
|
||||
|
||||
## Installing Plane
|
||||
### Requirements
|
||||
|
||||
Installing Plane is a very easy and minimal step process.
|
||||
- Operating systems: Debian, Ubuntu, CentOS
|
||||
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
|
||||
|
||||
### Prerequisite
|
||||
### Download the latest stable release
|
||||
|
||||
- Operating System (latest): Debian / Ubuntu / Centos
|
||||
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
|
||||
|
||||
### Downloading Latest Stable Release
|
||||
Run ↓ on any CLI.
|
||||
|
||||
```
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Downloading Preview Release</summary>
|
||||
### Download the Preview release
|
||||
|
||||
`Preview` builds do not support ARM64, AArch64 CPU architectures
|
||||
|
||||
Run ↓ on any CLI.
|
||||
|
||||
```
|
||||
export BRANCH=preview
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
|
||||
|
||||
```
|
||||
|
||||
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
|
||||
|
||||
</details>
|
||||
|
||||
--
|
||||
|
||||
Expect this after a successful install
|
||||
|
||||

|
||||
|
||||
Access the application on a browser via http://server-ip-address
|
||||
|
||||
---
|
||||
|
||||
### Get Control of your Plane Server Setup
|
||||
### Successful installation
|
||||
|
||||
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
|
||||
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Manage your Plane instance
|
||||
|
||||
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
|
||||
|
||||

|
||||
|
||||
<ins>Basic Operations</ins>:
|
||||
1. Basic operators
|
||||
|
||||
1. Start Server using `plane-app start`
|
||||
1. Stop Server using `plane-app stop`
|
||||
1. Restart Server using `plane-app restart`
|
||||
1. `plane-app start` starts the Plane server.
|
||||
2. `plane-app restart` restarts the Plane server.
|
||||
3. `plane-app stop` stops the Plane server.
|
||||
|
||||
<ins>Advanced Operations</ins>:
|
||||
2. Advanced operators
|
||||
|
||||
1. Configure Plane using `plane-app --configure`. This will give you options to modify
|
||||
`plane-app --configure` will show advanced configurators.
|
||||
|
||||
- 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)
|
||||
- Change your proxy or listening port
|
||||
<br>Default: 80
|
||||
- Change your domain name
|
||||
<br>Default: Deployed server's public IP address
|
||||
- File upload size
|
||||
<br>Default: 5MB
|
||||
- Specify external database address when using an external database
|
||||
<br>Default: `Empty`
|
||||
<br>`Default folder: /opt/plane/data/postgres`
|
||||
- Specify external Redis URL when using external Redis
|
||||
<br>Default: `Empty`
|
||||
<br>`Default folder: /opt/plane/data/redis`
|
||||
- Configure AWS S3 bucket
|
||||
<br>Use only when you or your users want to use S3
|
||||
<br>`Default folder: /opt/plane/data/minio`
|
||||
|
||||
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
|
||||
3. Version operators
|
||||
|
||||
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. Plane App can be reinstalled using `plane-app --install`.
|
||||
|
||||
<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. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
|
||||
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
|
||||
3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
|
||||
Postgres, Redis, and Minio alone.
|
||||
4. `plane-app --install` installs the Plane app again.
|
||||
|
||||
@@ -494,13 +494,6 @@ function install() {
|
||||
update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP"
|
||||
|
||||
update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')"
|
||||
|
||||
if command -v crontab &> /dev/null; then
|
||||
sudo touch /etc/cron.daily/makeplane
|
||||
sudo chmod +x /etc/cron.daily/makeplane
|
||||
sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane
|
||||
sudo crontab /etc/cron.daily/makeplane
|
||||
fi
|
||||
|
||||
show_message "Plane Installed Successfully ✅"
|
||||
show_message ""
|
||||
@@ -606,11 +599,6 @@ function uninstall() {
|
||||
sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null
|
||||
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
|
||||
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
|
||||
|
||||
if command -v crontab &> /dev/null; then
|
||||
sudo crontab -r &> /dev/null
|
||||
sudo rm /etc/cron.daily/makeplane &> /dev/null
|
||||
fi
|
||||
|
||||
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
|
||||
show_message "- Configuration Cleaned ✅"
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
top: 0;
|
||||
bottom: -2px;
|
||||
width: 4px;
|
||||
z-index: 5;
|
||||
z-index: 99;
|
||||
background-color: #d9e4ff;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -111,7 +111,7 @@
|
||||
.tableWrapper .tableControls .rowsControl {
|
||||
transition: opacity ease-in 100ms;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
z-index: 99;
|
||||
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-80), 0.6);
|
||||
background-color: rgba(var(--color-background-100), 0.5);
|
||||
}
|
||||
|
||||
.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer,
|
||||
|
||||
@@ -15,15 +15,9 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
return false;
|
||||
}
|
||||
|
||||
let a = event.target as HTMLElement;
|
||||
const els = [];
|
||||
const eventTarget = event.target as HTMLElement;
|
||||
|
||||
while (a.nodeName !== "DIV") {
|
||||
els.push(a);
|
||||
a = a.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
if (!els.find((value) => value.nodeName === "A")) {
|
||||
if (eventTarget.nodeName !== "A") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,7 +28,9 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
const target = link?.target ?? attrs.target;
|
||||
|
||||
if (link && href) {
|
||||
window.open(href, target);
|
||||
if (view.editable) {
|
||||
window.open(href, target);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -33,8 +33,16 @@ 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: link.href,
|
||||
href: url,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
+33
-62
@@ -1,76 +1,41 @@
|
||||
import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core";
|
||||
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||
import { autolink } from "./helpers/autolink";
|
||||
import { clickHandler } from "./helpers/clickHandler";
|
||||
import { pasteHandler } from "./helpers/pasteHandler";
|
||||
|
||||
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";
|
||||
|
||||
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;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>;
|
||||
openOnClick: boolean;
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -185,31 +150,37 @@ export const CustomLinkExtension = Mark.create<LinkOptions>({
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
find: (text) => {
|
||||
const foundLinks: PasteRuleMatch[] = [];
|
||||
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="([^"]*)"/;
|
||||
|
||||
if (text) {
|
||||
const links = find(text).filter((item) => item.isLink);
|
||||
const existingLink = html?.match(hrefRegex);
|
||||
|
||||
if (links.length) {
|
||||
links.forEach((link) =>
|
||||
foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href,
|
||||
},
|
||||
index: link.start,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existingLink) {
|
||||
return {
|
||||
href: existingLink[1],
|
||||
};
|
||||
}
|
||||
|
||||
return foundLinks;
|
||||
return {
|
||||
href: match.data?.href,
|
||||
};
|
||||
},
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
href: match.data?.href,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
},
|
||||
@@ -1,111 +0,0 @@
|
||||
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 it’s 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,7 +27,6 @@ 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: {
|
||||
@@ -56,7 +55,9 @@ export const CoreEditorExtensions = (
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
horizontalRule: {
|
||||
HTMLAttributes: { class: "mt-4 mb-4" },
|
||||
},
|
||||
blockquote: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
@@ -66,10 +67,6 @@ 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,11 +213,10 @@ function createToolbox({
|
||||
{ className: "colorPicker grid" },
|
||||
Object.entries(colors).map(([colorName, colorValue]) =>
|
||||
h("div", {
|
||||
className: "colorPickerItem flex items-center justify-center",
|
||||
className: "colorPickerItem",
|
||||
style: `background-color: ${colorValue.backgroundColor};
|
||||
color: ${colorValue.textColor || "inherit"};`,
|
||||
innerHTML:
|
||||
colorValue.icon ?? `<span class="text-md" style:"color: ${colorValue.backgroundColor}>A</span>`,
|
||||
color: ${colorValue.textColor || "inherit"};`,
|
||||
innerHTML: colorValue?.icon || "",
|
||||
onClick: () => onSelectColor(colorValue),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@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",
|
||||
@@ -46,7 +47,7 @@
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint": "8.36.0",
|
||||
"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">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AlertLabel } from "src/ui/components/alert-label";
|
||||
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "src/ui/components/vertical-dropdown-menu";
|
||||
import { SummaryPopover } from "src/ui/components/summary-popover";
|
||||
import { InfoPopover } from "src/ui/components/info-popover";
|
||||
import { getDate } from "src/utils/date-utils";
|
||||
|
||||
interface IEditorHeader {
|
||||
editor: Editor;
|
||||
@@ -72,7 +73,7 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
Icon={Archive}
|
||||
backgroundColor="bg-blue-500/20"
|
||||
textColor="text-blue-500"
|
||||
label={`Archived at ${new Date(archivedAt).toLocaleString()}`}
|
||||
label={`Archived at ${getDate(archivedAt)?.toLocaleString()}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import { usePopper } from "react-popper";
|
||||
import { Calendar, History, Info } from "lucide-react";
|
||||
// types
|
||||
import { DocumentDetails } from "src/types/editor-types";
|
||||
//utils
|
||||
import { getDate } from "src/utils/date-utils";
|
||||
|
||||
type Props = {
|
||||
documentDetails: DocumentDetails;
|
||||
};
|
||||
|
||||
// function to render a Date in the format- 25 May 2023 at 2:53PM
|
||||
const renderDate = (date: Date): string => {
|
||||
const renderDate = (date: Date | undefined): string => {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
@@ -52,14 +54,14 @@ export const InfoPopover: React.FC<Props> = (props) => {
|
||||
<h6 className="text-xs text-custom-text-400">Last updated on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<History className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.last_updated_at))}
|
||||
{renderDate(getDate(documentDetails?.last_updated_at))}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-xs text-custom-text-400">Created on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.created_on))}
|
||||
{renderDate(getDate(documentDetails?.created_on))}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,9 +33,8 @@ 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,3 +26,4 @@ export const DocumentEditorExtensions = (
|
||||
}),
|
||||
IssueWidgetPlaceholder(),
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
function isNumber(value: any) {
|
||||
return typeof value === "number";
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a date from string of type yyyy-mm-dd
|
||||
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
|
||||
* @param date
|
||||
* @returns date or undefined
|
||||
*/
|
||||
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
|
||||
if (!date || date === "") return;
|
||||
|
||||
if (typeof date !== "string" && !(date instanceof String)) return date;
|
||||
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
|
||||
const year = parseInt(yearString);
|
||||
const month = parseInt(monthString);
|
||||
const day = parseInt(dayString);
|
||||
if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return;
|
||||
|
||||
return new Date(year, month - 1, day);
|
||||
};
|
||||
@@ -33,6 +33,7 @@
|
||||
"@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"
|
||||
},
|
||||
@@ -40,7 +41,7 @@
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint": "8.36.0",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsconfig": "*",
|
||||
|
||||
@@ -36,9 +36,10 @@
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"eslint-config-custom": "*",
|
||||
"eslint": "^7.32.0",
|
||||
"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-config-custom": "*",
|
||||
"eslint": "^7.32.0",
|
||||
"postcss": "^8.4.29",
|
||||
"react": "^18.2.0",
|
||||
"tailwind-config-custom": "*",
|
||||
|
||||
@@ -1,43 +1,22 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"next",
|
||||
"turbo",
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
extends: ["next", "turbo", "prettier"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
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"],
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
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"],
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"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"],
|
||||
},
|
||||
],
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
"version": "0.16.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,31 +198,6 @@ 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: {
|
||||
|
||||
Vendored
+8
-1
@@ -1,4 +1,11 @@
|
||||
import type { TIssue, IIssueFilterOptions } from "@plane/types";
|
||||
import type {
|
||||
IUser,
|
||||
TIssue,
|
||||
IProjectLite,
|
||||
IWorkspaceLite,
|
||||
IIssueFilterOptions,
|
||||
IUserLite,
|
||||
} from "@plane/types";
|
||||
|
||||
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
|
||||
|
||||
|
||||
@@ -3,15 +3,6 @@ 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"
|
||||
@@ -24,27 +15,30 @@ 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 = {
|
||||
custom_dates?: string[];
|
||||
duration?: EDurationFilters;
|
||||
duration?: TDurationFilterOptions;
|
||||
tab?: TIssuesListTypes;
|
||||
};
|
||||
|
||||
export type TCreatedIssuesWidgetFilters = {
|
||||
custom_dates?: string[];
|
||||
duration?: EDurationFilters;
|
||||
duration?: TDurationFilterOptions;
|
||||
tab?: TIssuesListTypes;
|
||||
};
|
||||
|
||||
export type TIssuesByStateGroupsWidgetFilters = {
|
||||
duration?: EDurationFilters;
|
||||
custom_dates?: string[];
|
||||
duration?: TDurationFilterOptions;
|
||||
};
|
||||
|
||||
export type TIssuesByPriorityWidgetFilters = {
|
||||
custom_dates?: string[];
|
||||
duration?: EDurationFilters;
|
||||
duration?: TDurationFilterOptions;
|
||||
};
|
||||
|
||||
export type TWidgetFiltersFormData =
|
||||
@@ -103,12 +97,6 @@ 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 & {
|
||||
@@ -153,17 +141,8 @@ export type TRecentActivityWidgetResponse = IIssueActivity;
|
||||
export type TRecentProjectsWidgetResponse = string[];
|
||||
|
||||
export type TRecentCollaboratorsWidgetResponse = {
|
||||
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;
|
||||
active_issue_count: number;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
export type TWidgetStatsResponse =
|
||||
@@ -174,7 +153,7 @@ export type TWidgetStatsResponse =
|
||||
| TCreatedIssuesWidgetResponse
|
||||
| TRecentActivityWidgetResponse[]
|
||||
| TRecentProjectsWidgetResponse
|
||||
| TRecentCollaboratorsWidgetResponse;
|
||||
| TRecentCollaboratorsWidgetResponse[];
|
||||
|
||||
// dashboard
|
||||
export type TDashboard = {
|
||||
@@ -1,6 +0,0 @@
|
||||
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,6 +33,34 @@ 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;
|
||||
Vendored
+1
-2
@@ -1,3 +1,2 @@
|
||||
export * from "./inbox-issue";
|
||||
export * from "./inbox-types";
|
||||
export * from "./inbox";
|
||||
export * from "./inbox-issue";
|
||||
|
||||
Vendored
+10
@@ -4,6 +4,7 @@ export * from "./cycles";
|
||||
export * from "./dashboard";
|
||||
export * from "./projects";
|
||||
export * from "./state";
|
||||
export * from "./invitation";
|
||||
export * from "./issues";
|
||||
export * from "./modules";
|
||||
export * from "./views";
|
||||
@@ -14,6 +15,7 @@ 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";
|
||||
@@ -29,3 +31,11 @@ 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)];
|
||||
|
||||
Vendored
+14
-14
@@ -1,12 +1,16 @@
|
||||
import type { TIssue, IIssueFilterOptions, ILinkDetails } from "@plane/types";
|
||||
import type {
|
||||
IUser,
|
||||
IUserLite,
|
||||
TIssue,
|
||||
IProject,
|
||||
IWorkspace,
|
||||
IWorkspaceLite,
|
||||
IProjectLite,
|
||||
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;
|
||||
@@ -64,10 +68,6 @@ 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;
|
||||
|
||||
Vendored
+3
-8
@@ -1,17 +1,12 @@
|
||||
// types
|
||||
import {
|
||||
TIssue,
|
||||
IIssueLabel,
|
||||
IWorkspaceLite,
|
||||
IProjectLite,
|
||||
} from "@plane/types";
|
||||
import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types";
|
||||
|
||||
export interface IPage {
|
||||
access: number;
|
||||
archived_at: string | null;
|
||||
blocks: IPageBlock[];
|
||||
color: string;
|
||||
created_at: Date;
|
||||
created_at: string | null;
|
||||
created_by: string;
|
||||
description: string;
|
||||
description_html: string;
|
||||
@@ -25,7 +20,7 @@ export interface IPage {
|
||||
owned_by: string;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
updated_at: Date;
|
||||
updated_at: string | null;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
|
||||
Vendored
+12
-15
@@ -1,26 +1,12 @@
|
||||
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;
|
||||
@@ -35,13 +21,24 @@ 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;
|
||||
|
||||
Vendored
+1
-6
@@ -1,11 +1,6 @@
|
||||
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;
|
||||
|
||||
Vendored
+23
-7
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
IIssueActivity,
|
||||
TIssuePriorities,
|
||||
TStateGroups,
|
||||
EUserProjectRoles,
|
||||
} from ".";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { IIssueActivity, IIssueLite, TStateGroups } from ".";
|
||||
|
||||
export interface IUser {
|
||||
id: string;
|
||||
@@ -21,6 +17,7 @@ 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: {
|
||||
@@ -83,7 +80,7 @@ export interface IUserActivity {
|
||||
}
|
||||
|
||||
export interface IUserPriorityDistribution {
|
||||
priority: TIssuePriorities;
|
||||
priority: string;
|
||||
priority_count: number;
|
||||
}
|
||||
|
||||
@@ -92,6 +89,21 @@ 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;
|
||||
@@ -132,7 +144,11 @@ 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: {
|
||||
|
||||
Vendored
+1
-5
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "./view-props";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props";
|
||||
|
||||
export interface IProjectView {
|
||||
id: string;
|
||||
|
||||
@@ -23,11 +23,9 @@
|
||||
"@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": {
|
||||
|
||||
@@ -122,14 +122,14 @@ export const badgeStyling: IBadgeStyling = {
|
||||
};
|
||||
|
||||
export const getBadgeStyling = (variant: TBadgeVariant, size: TBadgeSizes, disabled: boolean = false): string => {
|
||||
let tempVariant: string = ``;
|
||||
let _variant: string = ``;
|
||||
const currentVariant = badgeStyling[variant];
|
||||
|
||||
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover}`;
|
||||
_variant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover}`;
|
||||
|
||||
let tempSize: string = ``;
|
||||
if (size) tempSize = badgeSizeStyling[size];
|
||||
return `${tempVariant} ${tempSize}`;
|
||||
let _size: string = ``;
|
||||
if (size) _size = badgeSizeStyling[size];
|
||||
return `${_variant} ${_size}`;
|
||||
};
|
||||
|
||||
export const getIconStyling = (size: TBadgeSizes): string => {
|
||||
|
||||
@@ -29,10 +29,13 @@ 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>
|
||||
@@ -43,11 +46,7 @@ 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>
|
||||
@@ -71,4 +70,4 @@ const BreadcrumbItem: React.FC<Props> = (props) => {
|
||||
|
||||
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
|
||||
|
||||
export { Breadcrumbs, BreadcrumbItem };
|
||||
export { Breadcrumbs, BreadcrumbItem };
|
||||
@@ -100,16 +100,16 @@ export const buttonStyling: IButtonStyling = {
|
||||
};
|
||||
|
||||
export const getButtonStyling = (variant: TButtonVariant, size: TButtonSizes, disabled: boolean = false): string => {
|
||||
let tempVariant: string = ``;
|
||||
let _variant: string = ``;
|
||||
const currentVariant = buttonStyling[variant];
|
||||
|
||||
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
|
||||
_variant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover} ${
|
||||
currentVariant.pressed
|
||||
}`;
|
||||
|
||||
let tempSize: string = ``;
|
||||
if (size) tempSize = buttonSizeStyling[size];
|
||||
return `${tempVariant} ${tempSize}`;
|
||||
let _size: string = ``;
|
||||
if (size) _size = buttonSizeStyling[size];
|
||||
return `${_variant} ${_size}`;
|
||||
};
|
||||
|
||||
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 handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
const _onClick = (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={handleOnClick} {...rest}>
|
||||
<a href={href} target={target} onClick={_onClick} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,605 +0,0 @@
|
||||
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 +0,0 @@
|
||||
export * from "./emoji-icon-picker";
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as React from "react";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
mode?: "primary" | "transparent" | "true-transparent";
|
||||
@@ -18,20 +16,17 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
ref={ref}
|
||||
type={type}
|
||||
name={name}
|
||||
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
|
||||
)}
|
||||
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}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
export * from "./avatar";
|
||||
export * from "./badge";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./badge";
|
||||
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";
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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,2 +1 @@
|
||||
export * from "./circular-spinner";
|
||||
export * from "./circular-bar-spinner";
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
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 +0,0 @@
|
||||
export * from "./icon-tabs";
|
||||
@@ -1,206 +0,0 @@
|
||||
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,2 +1 @@
|
||||
export * from "./latest-feature-block";
|
||||
export * from "./project-logo";
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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 />;
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
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";
|
||||
@@ -16,6 +19,18 @@ 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,
|
||||
@@ -108,15 +123,27 @@ 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">
|
||||
{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="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>
|
||||
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
|
||||
{projectStore?.project?.name || `...`}
|
||||
</div>
|
||||
|
||||
@@ -67,13 +67,13 @@ const DropdownList: React.FC<DropDownListProps> = (props) => {
|
||||
|
||||
const DropdownItem: React.FC<DropdownItemProps> = (props) => {
|
||||
const { item } = props;
|
||||
const { display, children, as: itemAs, href, onClick, isSelected } = item;
|
||||
const { display, children, as: as_, href, onClick, isSelected } = item;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="group relative flex w-full gap-x-6 rounded-lg p-1">
|
||||
{(!itemAs || itemAs === "button" || itemAs === "div") && (
|
||||
{(!as_ || as_ === "button" || as_ === "div") && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -94,7 +94,7 @@ const DropdownItem: React.FC<DropdownItemProps> = (props) => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{itemAs === "link" && <Link href={href || "#"}>{display}</Link>}
|
||||
{as_ === "link" && <Link href={href || "#"}>{display}</Link>}
|
||||
|
||||
{children && <DropdownList open={open} handleClose={() => setOpen(false)} items={children} />}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,10 @@ let rootStore: RootStore = new RootStore();
|
||||
export const MobxStoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
const initializeStore = () => {
|
||||
const singletonRootStore: RootStore = rootStore ?? new RootStore();
|
||||
if (typeof window === "undefined") return singletonRootStore;
|
||||
if (!rootStore) rootStore = singletonRootStore;
|
||||
return singletonRootStore;
|
||||
const _rootStore: RootStore = rootStore ?? new RootStore();
|
||||
if (typeof window === "undefined") return _rootStore;
|
||||
if (!rootStore) rootStore = _rootStore;
|
||||
return _rootStore;
|
||||
};
|
||||
|
||||
export const MobxStoreProvider = ({ children }: any) => {
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
"@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": "*"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TProjectLogoProps } from "@plane/types";
|
||||
|
||||
export interface IWorkspace {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -11,8 +9,10 @@ export interface IProject {
|
||||
identifier: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
cover_image: string | null;
|
||||
logo_props: TProjectLogoProps;
|
||||
icon_prop: string | null;
|
||||
emoji: string | null;
|
||||
}
|
||||
|
||||
export interface IProjectSettings {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_DEBUG",
|
||||
"JITSU_TRACKER_ACCESS_KEY",
|
||||
"JITSU_TRACKER_HOST"
|
||||
],
|
||||
|
||||
@@ -1,103 +1,4 @@
|
||||
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,13 +1,15 @@
|
||||
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
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { useUser } from "hooks/store";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +26,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -36,8 +39,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
|
||||
await deactivateAccount()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Account deactivated successfully.",
|
||||
});
|
||||
@@ -47,8 +50,8 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
handleClose();
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error,
|
||||
})
|
||||
@@ -86,11 +89,8 @@ 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="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 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>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="my-4 text-2xl font-medium leading-6 text-custom-text-100">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
// services
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import { useApplication } from "hooks/store";
|
||||
// ui
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
|
||||
|
||||
type Props = {
|
||||
handleSignInRedirection: () => Promise<void>;
|
||||
@@ -16,6 +17,8 @@ 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 },
|
||||
@@ -36,9 +39,9 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
if (response) handleSignInRedirection();
|
||||
} else throw Error("Cant find credentials");
|
||||
} catch (err: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
title: "Error signing in!",
|
||||
type: "error",
|
||||
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
@@ -57,9 +60,9 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
||||
if (response) handleSignInRedirection();
|
||||
} else throw Error("Cant find credentials");
|
||||
} catch (err: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
title: "Error signing in!",
|
||||
type: "error",
|
||||
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ 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, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
@@ -25,6 +27,7 @@ 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 },
|
||||
@@ -49,8 +52,8 @@ export const SignInEmailForm: React.FC<Props> = observer((props) => {
|
||||
.emailCheck(payload)
|
||||
.then((res) => onSubmit(res.is_password_autoset))
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
|
||||
@@ -3,9 +3,10 @@ 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, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// icons
|
||||
@@ -37,6 +38,8 @@ 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,
|
||||
@@ -59,8 +62,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
await authService
|
||||
.setPassword(payload)
|
||||
.then(async () => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Password created successfully.",
|
||||
});
|
||||
@@ -75,8 +78,8 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
state: "FAILED",
|
||||
first_time: false,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
@@ -157,7 +160,7 @@ export const SignInOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
Whatever you choose now will be your account{"'"}s password until you change it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
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;
|
||||
@@ -42,6 +43,8 @@ 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();
|
||||
@@ -80,8 +83,8 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
||||
await onSubmit();
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
@@ -104,8 +107,8 @@ export const SignInPasswordForm: React.FC<Props> = observer((props) => {
|
||||
.generateUniqueCode({ email: emailFormValue })
|
||||
.then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD))
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker } from "hooks/store";
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import { LatestFeatureBlock } from "components/common";
|
||||
import {
|
||||
SignInEmailForm,
|
||||
SignInUniqueCodeForm,
|
||||
@@ -9,12 +13,8 @@ import {
|
||||
OAuthOptions,
|
||||
SignInOptionalSetPasswordForm,
|
||||
} from "components/account";
|
||||
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
|
||||
import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker";
|
||||
|
||||
export enum ESignInSteps {
|
||||
EMAIL = "EMAIL",
|
||||
|
||||
@@ -2,21 +2,20 @@ 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;
|
||||
@@ -43,6 +42,8 @@ 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
|
||||
@@ -83,8 +84,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
captureEvent(CODE_VERIFIED, {
|
||||
state: "FAILED",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
@@ -100,8 +101,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => {
|
||||
setResendCodeTimer(30);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "A new unique code has been sent to your email.",
|
||||
});
|
||||
@@ -112,8 +113,8 @@ export const SignInUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
});
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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 { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// types
|
||||
import { IEmailCheckData } from "@plane/types";
|
||||
|
||||
@@ -25,6 +27,7 @@ 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 },
|
||||
@@ -49,8 +52,8 @@ export const SignUpEmailForm: React.FC<Props> = observer((props) => {
|
||||
.emailCheck(payload)
|
||||
.then(() => onSubmit())
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
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
|
||||
// components
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
// 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,6 +41,8 @@ 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,
|
||||
@@ -63,8 +65,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
await authService
|
||||
.setPassword(payload)
|
||||
.then(async () => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Password created successfully.",
|
||||
});
|
||||
@@ -79,8 +81,8 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
state: "FAILED",
|
||||
first_time: true,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
@@ -162,7 +164,7 @@ export const SignUpOptionalSetPasswordForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
This password will continue to be your account{"'"}s password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
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, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkEmailValidity } from "helpers/string.helper";
|
||||
import { AuthService } from "services/auth.service";
|
||||
// types
|
||||
import { IPasswordSignInData } from "@plane/types";
|
||||
|
||||
@@ -32,6 +34,8 @@ 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,
|
||||
@@ -55,8 +59,8 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
|
||||
.passwordSignIn(payload)
|
||||
.then(async () => await onSubmit())
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
@@ -134,7 +138,7 @@ export const SignUpPasswordForm: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<p className="mt-2 pb-3 text-xs text-onboarding-text-200">
|
||||
<p className="text-onboarding-text-200 text-xs mt-2 pb-3">
|
||||
This password will continue to be your account{"'"}s password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import Link from "next/link";
|
||||
import { useApplication, useEventTracker } from "hooks/store";
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import {
|
||||
OAuthOptions,
|
||||
SignUpEmailForm,
|
||||
@@ -9,11 +11,9 @@ import {
|
||||
SignUpPasswordForm,
|
||||
SignUpUniqueCodeForm,
|
||||
} from "components/account";
|
||||
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
|
||||
import { useApplication, useEventTracker } from "hooks/store";
|
||||
import useSignInRedirection from "hooks/use-sign-in-redirection";
|
||||
// components
|
||||
import Link from "next/link";
|
||||
// constants
|
||||
import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker";
|
||||
|
||||
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,6 +44,8 @@ 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
|
||||
@@ -82,8 +84,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
captureEvent(CODE_VERIFIED, {
|
||||
state: "FAILED",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
@@ -99,8 +101,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => {
|
||||
setResendCodeTimer(30);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "A new unique code has been sent to your email.",
|
||||
});
|
||||
@@ -110,8 +112,8 @@ export const SignUpUniqueCodeForm: React.FC<Props> = (props) => {
|
||||
});
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// 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 { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||
import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||
import { generateBarColor, generateDisplayName } from "helpers/analytics.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 vertical-scrollbar scrollbar-md">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<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,9 +22,8 @@ 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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user