Compare commits

...
Author SHA1 Message Date
rahulramesha f8a5095f14 fix date related exceptions 2024-03-19 19:57:32 +05:30
rahulrameshaandGitHub 1a462711e1 [WEB-761] fix: Time zone date misalignment (#3986)
* fix date time misalignment

* fix failing build

* fix gantt chart view position fix

* comments for getDate method

* remove new Date() where not required

* changes from my self review
2024-03-19 17:40:20 +05:30
sriram veeraghanta aa3702cd46 fix: codeql check workflow changes 2024-03-18 19:50:58 +05:30
sriram veeraghanta aed87ef472 fix: build branch workflow dispatch trigger changes 2024-03-18 19:48:44 +05:30
Aaryan KhandelwalandGitHub aaad37575b fix: cycle date check wrong projectId (#3972) 2024-03-14 20:11:10 +05:30
Bavisetti NarayanandGitHub 42b524b16a chore: project active filter (#3971) 2024-03-14 19:22:54 +05:30
Anmol Singh BhatiaandGitHub 0bc4b6cece chore: ai modal disclamier added (#3970) 2024-03-14 19:16:07 +05:30
guru_sainathandGitHub 43c75f4457 fix: rendering Issue in kanban swimlanes view for unassigned cycle or Modules (#3965) 2024-03-14 12:43:29 +05:30
sriram veeraghanta 9c13dbd957 chore: 1click docs readme update 2024-03-13 17:26:52 +05:30
sriram veeraghanta 8d9adf4d87 chore: 1click docs readme update 2024-03-13 17:26:04 +05:30
sriram veeraghanta 4ccb505f36 chore: 1click docs changes 2024-03-13 17:24:37 +05:30
sriram veeraghanta 884856b021 chore: 1click docs update 2024-03-13 17:18:11 +05:30
sriram veeraghanta 552c66457a chore: 1click docs update 2024-03-13 17:17:40 +05:30
P BandGitHub 1363ef0b2d Updated one-click deploy's readme for readability and presentation (#3962)
Changed title, first paragraph, and bullets.
2024-03-13 17:13:11 +05:30
guru_sainathandGitHub 898cf98be3 [WEB-749] fix: rendering empty states with "showEmptyGroup" filter in issue grouping (#3954)
* fix: Fixed show empty states in groupBy and subGroupBy in kanban

* lint: lint issue resolved
2024-03-13 16:59:12 +05:30
sriram veeraghanta 6ec9c64f7c fix: build branch optional builds workflow changes 2024-03-12 01:06:37 +05:30
sriram veeraghanta f493a03f56 fix: adding github workflow events 2024-03-12 01:01:03 +05:30
sriram veeraghanta cb78ccad1f fix: 1click deployment fixes 2024-03-08 16:58:37 +05:30
Manish Gupta 6c6b7156bb helm variable update 2024-03-08 16:00:18 +05:30
Manish Gupta 8cc372679c Web Build fixes 2024-03-08 15:56:05 +05:30
Manish GuptaandGitHub 94327b8311 chore: feature build process optimization (#3907)
* process changed to build tar and use for deployment

* fixes
2024-03-08 15:16:32 +05:30
Anmol Singh BhatiaandGitHub 2b05d23470 fix: kanban card state overflow fix (#3866) 2024-03-06 14:28:24 +05:30
sriram veeraghanta 73334130be Merge branch 'chore/auto-merge' of github.com:makeplane/plane into preview 2024-03-06 14:07:34 +05:30
4d0f641ee0 [WEB-588] chore: remove the word title from the issue title tooltip. (#3874)
* [WEB-588] chore: remove the word `title` from the issue title tooltip.

* fix: github url fixes in feature deploy action

---------

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

* chore: variable name changes

---------

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

* Update build-test-pull-request.yml
2024-03-05 13:14:00 +05:30
Henit ChobisaandGitHub af70722e34 chore: added workflow for checking version before merge to master (#3847) 2024-03-05 13:02:13 +05:30
M. PalanikannanandGitHub d99529b109 fix: crash while updating link text on the last node (#3871) 2024-03-04 20:33:16 +05:30
Vihar KuramaandGitHub 6eb7014ea4 chore: update github readme - content edits to feature section, new screenshots and added repo activity, contributors. (#3870)
* update github readme, add new features and deployment options

* add new product screenshots

* add contributors section to readme

* chore: update formatting
2024-03-04 20:06:53 +05:30
Henit Chobisa 59c9b3bdce chore: added auto merge CI for merging sync branches 2024-03-04 15:13:39 +05:30
Anmol Singh BhatiaandGitHub 894de80f41 [WEB-616] fix: inbox issue navigation issue title and description event propagation (#3851)
* fix: inbox issue navigation issue title and description event propagation

* fix: inbox issue navigation issue title and description event propagation
2024-03-01 17:29:30 +05:30
Anmol Singh BhatiaandGitHub 4b706437d7 [WEB-619] fix: workspace all issue quick action (#3853)
* chore: custom menu dropdown menu items classname prop added

* fix: issue layout quick action z index fix
2024-03-01 17:23:26 +05:30
Anmol Singh BhatiaandGitHub e4bccea824 fix: multiple issue comment (#3854) 2024-03-01 17:20:36 +05:30
sriram veeraghanta d39f2526a2 chore: removing unnecessary lines 2024-03-01 13:49:50 +05:30
Bavisetti NarayanandGitHub bc6e48fcd6 chore: issue comment PATCH changes (#3850) 2024-03-01 13:33:23 +05:30
guru_sainathandGitHub e6f33eb262 [WEB-609] fix: header text overflow issue in kanban layout (#3848)
* fix: kanban header text overflow

* chore: updated condition
2024-02-29 20:29:02 +05:30
guru_sainathandGitHub 5cfebb8dae fix: issue description on last draft issue (#3846) 2024-02-29 19:41:30 +05:30
Anmol Singh BhatiaandGitHub e4988ee940 fix: issue sidebar and peek overview cycle select improvement (#3845) 2024-02-29 19:40:46 +05:30
sriram veeraghanta 850bf01d65 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-29 17:20:44 +05:30
Anmol Singh BhatiaandGitHub f183e389ea fix: module sidebar description duplicacy (#3844) 2024-02-29 17:20:17 +05:30
guru_sainathandGitHub 5d7c0a2a64 [WEB-600] fix: fixed sub-group-by issue count display in kanban layout header (#3843)
* fix: fixed subgroupby issue count at the header in kanban layout

* chore: code beautification
2024-02-29 17:19:51 +05:30
Anmol Singh BhatiaandGitHub 92e994990c fix: project sidebar favorite list logic updated (#3842) 2024-02-29 17:19:13 +05:30
guru_sainathandGitHub d1087820f6 [web-599] ui: kanban layout UI consistency enhancement for grouped display filters (#3841)
* ui: UI inconsistancy in kanban layout when we grouped and sub grouped display filters.

* ui: width update in the kanban block
2024-02-29 17:18:03 +05:30
Anmol Singh BhatiaandGitHub 65024fe5ec chore: priority icon none state hover state added (#3840) 2024-02-29 16:12:34 +05:30
Anmol Singh BhatiaandGitHub 62693abb09 chore: project emoji icon improvement (#3837) 2024-02-29 15:31:44 +05:30
guru_sainathandGitHub 9326fb0762 [WEB-601] feat: enhanced display filters grouping by cycles and modules in project issues (#3834)
* feat: implemented cycle and module for display filters groupBy and sunGroupBy in  project issues list and kanban layouts

* chore: Enabled drag ability for cycle and handled prepopulated data for quick add

* chore: disbaled drag ability for cycle

* chore: updated preloaded data

* chore: updated module and cycle store router dependancy to prop dependancy
2024-02-29 15:31:03 +05:30
56805203f1 [WEB-597] chore: dashboard overdue issues (#3832)
* fix bug of dashboard report overdue of all tasks #3815 (#3823)

* chore: pending issues filter

* chore: removed the Q parameter

* chore: dashboard widget overdue stats card redirection params updated

---------

Co-authored-by: AbId KhAn <abidkhan484@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-02-29 13:32:16 +05:30
Anmol Singh BhatiaandGitHub 39136d3a9f chore: spreadsheet layout dropdowns focus on toggle functionality added (#3833) 2024-02-29 13:23:37 +05:30
Anmol Singh BhatiaandGitHub 34301e4399 fix: app sidebar toggle (#3829) 2024-02-28 19:55:22 +05:30
guru_sainathandGitHub 51f795fbd7 [WEB-477] feat: enhanced project issue filtering by cycles and modules (#3830)
* feat: implemented cycle and module filter in project issues

* feat: implemented cycle and module filter in draft and archived issues
2024-02-28 19:34:29 +05:30
Anmol Singh BhatiaandGitHub 7abfbac479 chore: spreadsheet layout dropdown z index improvement (#3825) 2024-02-28 19:15:03 +05:30
Aaryan KhandelwalandGitHub c4028efd71 fix: usePage hook throws an error without projectId (#3827) 2024-02-28 19:14:15 +05:30
Aaryan KhandelwalandGitHub 0215b697a5 chore: update issue date icons (#3826) 2024-02-28 19:13:17 +05:30
126 changed files with 2274 additions and 1116 deletions
+97
View File
@@ -0,0 +1,97 @@
name: Auto Merge or Create PR on Push
on:
workflow_dispatch:
push:
branches:
- "sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce"
TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
REVIEWER: ${{ secrets.REVIEWER }}
jobs:
Check_Branch:
runs-on: ubuntu-latest
outputs:
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
steps:
- name: Check if current branch matches the secret
id: check-branch
run: |
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
echo "MATCH=true" >> $GITHUB_OUTPUT
else
echo "MATCH=false" >> $GITHUB_OUTPUT
fi
Auto_Merge:
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
needs: [Check_Branch]
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Setup GH CLI and Git Config
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- id: git-author
name: Setup Git CLI from Github Token
run: |
VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer')
VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}")
VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}")
VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}")
USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}"
USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com"
git config --global user.name ${USER_NAME}
git config --global user.email ${USER_EMAIL}
- name: Check for merge conflicts
id: conflicts
run: |
git fetch origin $TARGET_BRANCH
git checkout $TARGET_BRANCH
# Attempt to merge the main branch into the current branch
if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then
echo "No merge conflicts detected."
echo "HAS_CONFLICTS=false" >> $GITHUB_ENV
else
echo "Merge conflicts detected."
echo "HAS_CONFLICTS=true" >> $GITHUB_ENV
git merge --abort
fi
- name: Merge Change to Target Branch
if: env.HAS_CONFLICTS == 'false'
run: |
git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH"
git push origin $TARGET_BRANCH
- name: Create PR to Target Branch
if: env.HAS_CONFLICTS == 'true'
run: |
# Use GitHub CLI to create PR and specify author and committer
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \
--title "sync: merge conflicts need to be resolved" \
--body "" \
--reviewer $REVIEWER )
echo "Pull Request created: $PR_URL"
+5 -6
View File
@@ -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 }}
+84 -27
View File
@@ -1,27 +1,19 @@
name: Build Pull Request Contents
name: Build and Lint on Pull Request
on:
workflow_dispatch:
pull_request:
types: ["opened", "synchronize"]
jobs:
build-pull-request-contents:
name: Build Pull Request Contents
runs-on: ubuntu-20.04
permissions:
pull-requests: read
get-changed-files:
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }}
steps:
- name: Checkout Repository to Actions
uses: actions/checkout@v3.3.0
with:
token: ${{ secrets.ACCESS_TOKEN }}
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
- uses: actions/checkout@v3
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
@@ -31,17 +23,82 @@ jobs:
- apiserver/**
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
deploy:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
yarn
yarn build --filter=web
lint-apiserver:
needs: get-changed-files
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x' # Specify the Python version you need
- name: Install Pylint
run: python -m pip install ruff
- name: Install Apiserver Dependencies
run: cd apiserver && pip install -r requirements.txt
- name: Lint apiserver
run: ruff check --fix apiserver
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
yarn
yarn build --filter=space
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=web
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=space
build-web:
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=web
build-space:
needs: lint-space
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=space
+45
View File
@@ -0,0 +1,45 @@
name: Version Change Before Release
on:
pull_request:
branches:
- master
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Fetch base branch
run: git fetch origin master:master
- name: Get Master Branch version
run: |
git checkout master
echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Get master branch version and compare
run: |
echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION"
if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then
echo "Version in PR branch is the same as in master. Failing the CI."
exit 1
else
echo "Version check passed. Versions are different."
fi
env:
PR_VERSION: ${{ env.PR_VERSION }}
MASTER_VERSION: ${{ env.MASTER_VERSION }}
+31 -33
View File
@@ -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}}"
+199
View File
@@ -0,0 +1,199 @@
name: Feature Preview
on:
workflow_dispatch:
inputs:
web-build:
required: false
description: 'Build Web'
type: boolean
default: true
space-build:
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:
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/${{ 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: |
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
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
if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
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
+98 -53
View File
@@ -7,7 +7,7 @@
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Flexible, extensible open-source project management</b></p>
<p align="center"><b>Open-source project management that unlocks customer value.</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -16,6 +16,13 @@
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
</p>
<p align="center">
<a href="http://www.plane.so"><b>Website</b></a> •
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
<a href="https://docs.plane.so/"><b>Documentation</b></a>
</p>
<p>
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
<img
@@ -33,56 +40,93 @@
</a>
</p>
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️.
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
## ⚡️ Contributors Quick Start
### Prerequisite
## ⚡ Installation
Development system must have docker engine installed and running.
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.
### Steps
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).
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
| Installation Methods | Documentation Link |
|-----------------|----------------------------------------------------------------------------------------------------------|
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
1. Switch to the code folder `cd plane`
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
1. Open terminal and run `./setup.sh`
1. Open the code on VSCode or similar equivalent IDE
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
Thats it!
## 🍙 Self Hosting
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
## 🚀 Features
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Cycles**
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue.
- **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.
## 🛠️ Contributors Quick Start
> Development system must have docker engine installed and running.
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
1. Clone the code locally using:
```
git clone https://github.com/makeplane/plane.git
```
2. Switch to the code folder:
```
cd plane
```
3. Create your feature or fix branch you plan to work on using:
```
git checkout -b <feature-branch-name>
```
4. Open terminal and run:
```
./setup.sh
```
5. Open the code on VSCode or similar equivalent IDE.
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
```
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
Thats it!
## ❤️ Community
The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels.
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
### Repo Activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## 📸 Screenshots
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_views_dark_mode.webp"
src="https://ik.imagekit.io/w2okwbtu2/Issues_rNZjrGgFl.png?updatedAt=1709298765880"
alt="Plane Views"
width="100%"
/>
@@ -91,8 +135,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_issue_detail_dark_mode.webp"
alt="Plane Issue Details"
src="https://ik.imagekit.io/w2okwbtu2/Cycles_jCDhqmTl9.png?updatedAt=1709298780697"
width="100%"
/>
</a>
@@ -100,7 +143,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_cycles_modules_dark_mode.webp"
src="https://ik.imagekit.io/w2okwbtu2/Modules_PSCVsbSfI.png?updatedAt=1709298796783"
alt="Plane Cycles and Modules"
width="100%"
/>
@@ -109,7 +152,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_analytics_dark_mode.webp"
src="https://ik.imagekit.io/w2okwbtu2/Views_uxXsRatS4.png?updatedAt=1709298834522"
alt="Plane Analytics"
width="100%"
/>
@@ -118,7 +161,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_pages_dark_mode.webp"
src="https://ik.imagekit.io/w2okwbtu2/Analytics_0o22gLRtp.png?updatedAt=1709298834389"
alt="Plane Pages"
width="100%"
/>
@@ -128,7 +171,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_commad_k_dark_mode.webp"
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
alt="Plane Command Menu"
width="100%"
/>
@@ -136,20 +179,22 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.
</p>
</p>
## 📚Documentation
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md).
## ❤️ Community
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/A92xrEGCge).
Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels.
## ⛓️ 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. Email engineering@plane.so to disclose any security vulnerabilities.
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.
### 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>
+2 -2
View File
@@ -716,9 +716,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
# Validation check if the issue already exists
if (
str(request.data.get("external_id"))
request.data.get("external_id")
and (issue_comment.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
and IssueComment.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get(
+1
View File
@@ -58,6 +58,7 @@ def dashboard_overview_stats(self, request, slug):
pending_issues_count = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__lt=timezone.now().date(),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
+24 -14
View File
@@ -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)
+50 -49
View File
@@ -1,78 +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
![Install Output](images/install.png)
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.
![Install Output](images/install.png)
---
### Manage your Plane instance
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
![Plane Help](images/help.png)
<ins>Basic Operations</ins>:
1. Start Server using `plane-app start`
1. Stop Server using `plane-app stop`
1. Restart Server using `plane-app restart`
1. Basic operators
<ins>Advanced Operations</ins>:
1. Configure Plane using `plane-app --configure`. This will give you options to modify
- NGINX Port (default 80)
- Domain Name (default is the local server public IP address)
- File Upload Size (default 5MB)
- External Postgres DB Url (optional - default empty)
- External Redis URL (optional - default empty)
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
1. `plane-app start` starts the Plane server.
2. `plane-app restart` restarts the Plane server.
3. `plane-app stop` stops the Plane server.
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
2. Advanced operators
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
`plane-app --configure` will show advanced configurators.
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.
- 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. Plane App can be reinstalled using `plane-app --install`.
3. Version operators
<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.
-12
View File
@@ -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 ✅"
@@ -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>
@@ -40,9 +40,11 @@ export const LinkEditView = ({
const [positionRef, setPositionRef] = useState({ from: from, to: to });
const [localUrl, setLocalUrl] = useState(viewProps.url);
const linkRemoved = useRef<Boolean>();
const linkRemoved = useRef<boolean>();
const getText = (from: number, to: number) => {
if (to >= editor.state.doc.content.size) return "";
const text = editor.state.doc.textBetween(from, to, "\n");
return text;
};
@@ -72,10 +74,12 @@ export const LinkEditView = ({
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
if (to >= editor.state.doc.content.size) return;
editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link));
editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url })));
},
[localUrl]
[localUrl, editor, from, to, viewProps.url]
);
const handleUpdateText = (text: string) => {
@@ -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);
};
+2
View File
@@ -203,6 +203,8 @@ export interface ViewFlags {
export type GroupByColumnTypes =
| "project"
| "cycle"
| "module"
| "state"
| "state_detail.group"
| "priority"
+2 -2
View File
@@ -6,7 +6,7 @@ export interface IPage {
archived_at: string | null;
blocks: IPageBlock[];
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html: string;
@@ -20,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;
+6
View File
@@ -14,6 +14,8 @@ export type TIssueGroupByOptions =
| "project"
| "assignees"
| "mentions"
| "cycle"
| "module"
| null;
export type TIssueOrderByOptions =
@@ -60,6 +62,8 @@ export type TIssueParams =
| "created_by"
| "subscriber"
| "labels"
| "cycle"
| "module"
| "start_date"
| "target_date"
| "project"
@@ -79,6 +83,8 @@ export interface IIssueFilterOptions {
labels?: string[] | null;
priority?: string[] | null;
project?: string[] | null;
cycle?: string[] | null;
module?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
+2 -1
View File
@@ -27,6 +27,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
noBorder = false,
noChevron = false,
optionsClassName = "",
menuItemsClassName = "",
verticalEllipsis = false,
portalElement,
menuButtonOnClick,
@@ -70,7 +71,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
useOutsideClickDetector(dropdownRef, closeDropdown);
let menuItems = (
<Menu.Items className="fixed z-10" static>
<Menu.Items className={cn("fixed z-10", menuItemsClassName)} static>
<div
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
+1
View File
@@ -24,6 +24,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
noBorder?: boolean;
verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
menuItemsClassName?: string;
onMenuClose?: () => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
@@ -8,7 +8,7 @@ import { DateFilterSelect } from "./date-filter-select";
// ui
import { Button } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "helpers/date-time.helper";
type Props = {
title: string;
@@ -43,7 +43,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
handleClose();
};
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const date1 = getDate(watch("date1"));
const date2 = getDate(watch("date1"));
const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false;
return (
<Transition.Root show={isOpen} as={Fragment}>
@@ -86,35 +89,45 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
<Controller
control={control}
name="date1"
render={({ field: { value, onChange } }) => (
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ after: new Date(watch("date2")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date2Value = getDate(watch("date2"));
return (
<DayPicker
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date2Value ? [{ after: date2Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/>
);
}}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
render={({ field: { value, onChange } }) => (
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ before: new Date(watch("date1")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date1Value = getDate(watch("date1"));
return (
<DayPicker
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/>
);
}}
/>
)}
</div>
@@ -7,6 +7,8 @@ import useToast from "hooks/use-toast";
import { usePopper } from "react-popper";
// ui
import { Button, Input } from "@plane/ui";
// icons
import { AlertCircle } from "lucide-react";
// components
import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
import { Popover, Transition } from "@headlessui/react";
@@ -250,8 +252,17 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
/>
)}
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
{responseActionButton}
<div className="flex gap-2 justify-between">
{responseActionButton ? (
<>{responseActionButton}</>
) : (
<>
<div className="flex items-center justify-center gap-2 text-sm text-custom-primary">
<AlertCircle className="h-4 w-4" />
<p>By using this feature, you consent to sharing the message with a 3rd party service. </p>
</div>
</>
)}
<div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Close
@@ -3,7 +3,7 @@ import { eachDayOfInterval, isValid } from "date-fns";
// ui
import { LineGraph } from "components/ui";
// helpers
import { renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import { getDate, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
//types
import { TCompletionChartDistribution } from "@plane/types";
@@ -47,11 +47,11 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
}));
const generateXAxisTickValues = () => {
const start = new Date(startDate);
const end = new Date(endDate);
const start = getDate(startDate);
const end = getDate(endDate);
let dates: Date[] = [];
if (isValid(start) && isValid(end)) {
if (start && end && isValid(start) && isValid(end)) {
dates = eachDayOfInterval({ start, end });
}
@@ -5,17 +5,17 @@ import { observer } from "mobx-react";
type Props = {
onClick?: () => void;
}
};
export const SidebarHamburgerToggle: FC<Props> = observer((props) => {
const { onClick } = props
const { onClick } = props;
const { theme: themeStore } = useApplication();
return (
<div
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => {
if (onClick) onClick()
else themeStore.toggleMobileSidebar()
if (onClick) onClick();
else themeStore.toggleSidebar();
}}
>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
+11 -8
View File
@@ -27,7 +27,12 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers
import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import {
renderFormattedDate,
findHowManyDaysLeft,
renderFormattedDateWithoutYear,
getDate,
} from "helpers/date-time.helper";
import { truncateText } from "helpers/string.helper";
// types
import { ICycle, TCycleGroups } from "@plane/types";
@@ -102,8 +107,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
/>
);
const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? "");
const endDate = getDate(activeCycle.end_date);
const startDate = getDate(activeCycle.start_date);
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
const groupedIssues: any = {
backlog: activeCycle.backlog_issues,
@@ -113,8 +120,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cancelled: activeCycle.cancelled_issues,
};
const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
@@ -151,8 +156,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
color: group.color,
}));
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
return (
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
@@ -311,7 +314,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{currentProjectDetails?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
<Tooltip position="top-left" tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip position="top-left" tooltipContent={issue.name}>
<span className="text-[0.825rem] text-custom-text-100">{truncateText(issue.name, 30)}</span>
</Tooltip>
</div>
@@ -152,6 +152,7 @@ export const CycleMobileHeader = () => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
/>
</FiltersDropdown>
</div>
+3 -3
View File
@@ -12,7 +12,7 @@ import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon }
// icons
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
// helpers
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// constants
import { CYCLE_STATUS } from "constants/cycle";
@@ -50,8 +50,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const cycleStatus = cycleDetails.status.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const isDateValid = cycleDetails.start_date || cycleDetails.end_date;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
+3 -3
View File
@@ -12,7 +12,7 @@ import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarG
// icons
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
// helpers
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// constants
import { CYCLE_STATUS } from "constants/cycle";
@@ -137,8 +137,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
// TODO: change this logic once backend fix the response
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
+3 -3
View File
@@ -5,7 +5,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
@@ -135,8 +135,8 @@ export const CycleForm: React.FC<Props> = (props) => {
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -8,6 +8,7 @@ import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/
import { CycleGanttBlock } from "components/cycles";
// types
import { ICycle } from "@plane/types";
import { getDate } from "helpers/date-time.helper";
// constants
import { EUserProjectRoles } from "constants/project";
@@ -45,8 +46,8 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
data: block,
id: block?.id ?? "",
sort_order: block?.sort_order ?? 0,
start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block?.end_date ?? ""),
start_date: getDate(block?.start_date),
target_date: getDate(block?.end_date),
}));
return structuredBlocks;
+4 -4
View File
@@ -96,10 +96,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
};
const dateChecker = async (payload: CycleDateCheckData) => {
const dateChecker = async (projectId: string, payload: CycleDateCheckData) => {
let status = false;
await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload).then((res) => {
await cycleService.cycleDateCheck(workspaceSlug, projectId, payload).then((res) => {
status = res.status;
});
@@ -117,13 +117,13 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
if (payload.start_date && payload.end_date) {
if (data?.start_date && data?.end_date)
isDateValid = await dateChecker({
isDateValid = await dateChecker(payload.project_id ?? projectId, {
start_date: payload.start_date,
end_date: payload.end_date,
cycle_id: data.id,
});
else
isDateValid = await dateChecker({
isDateValid = await dateChecker(payload.project_id ?? projectId, {
start_date: payload.start_date,
end_date: payload.end_date,
});
+8 -5
View File
@@ -18,8 +18,8 @@ import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui";
// icons
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
// constants
@@ -186,8 +186,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const startDate = getDate(cycleDetails?.start_date);
const endDate = getDate(cycleDetails?.end_date);
const isStartValid = startDate && startDate <= new Date();
const isEndValid = endDate && startDate && endDate >= startDate;
const progressPercentage = cycleDetails
? isCompleted && cycleDetails?.progress_snapshot
@@ -317,8 +320,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -5,7 +5,7 @@ import { useIssueDetail, useMember, useProject } from "hooks/store";
// ui
import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui";
// helpers
import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper";
import { findTotalDaysInRange, getDate, renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue, TWidgetIssue } from "@plane/types";
@@ -34,6 +34,8 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const targetDate = getDate(issueDetails.target_date);
return (
<ControlLink
href={`/${workspaceSlug}/projects/${issueDetails.project_id}/issues/${issueDetails.id}`}
@@ -48,11 +50,7 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
<h6 className="text-sm flex-grow truncate">{issueDetails.name}</h6>
</div>
<div className="text-xs text-center">
{issueDetails.target_date
? isToday(new Date(issueDetails.target_date))
? "Today"
: renderFormattedDate(issueDetails.target_date)
: "-"}
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
</div>
<div className="text-xs text-center">
{blockedByIssues.length > 0
@@ -83,7 +81,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0;
return (
<ControlLink
@@ -212,7 +210,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
const projectDetails = getProjectById(issue.project_id);
const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0;
return (
<ControlLink
@@ -37,7 +37,7 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
key: "overdue",
title: "Issues overdue",
count: widgetStats?.pending_issues_count,
link: `/${workspaceSlug}/workspace-views/assigned/?target_date=${today};before`,
link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`,
},
{
key: "created",
+1
View File
@@ -67,6 +67,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
if (isOpen) onClose && onClose();
};
const dropdownOnChange = (val: string | null) => {
+3 -3
View File
@@ -9,7 +9,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
@@ -168,8 +168,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
{...attributes.popper}
>
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
selected={getDate(value)}
defaultMonth={getDate(value)}
onSelect={(date) => {
dropdownOnChange(date ?? null);
}}
@@ -67,6 +67,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
const toggleDropdown = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
if (isOpen) onClose && onClose();
};
const dropdownOnChange = (val: string & string[]) => {
+2 -2
View File
@@ -58,7 +58,7 @@ const BorderButton = (props: ButtonProps) => {
high: "bg-orange-500/20 text-orange-950 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
none: "bg-custom-background-80 border-custom-border-300",
none: "hover:bg-custom-background-80 border-custom-border-300",
};
return (
@@ -197,7 +197,7 @@ const TransparentButton = (props: ButtonProps) => {
high: "text-orange-950",
medium: "text-yellow-950",
low: "text-blue-950",
none: "",
none: "hover:text-custom-text-300",
};
return (
+1 -1
View File
@@ -53,7 +53,7 @@ const EmojiIconPicker: React.FC<Props> = (props) => {
<Popover.Button
ref={buttonRef}
onClick={() => setIsOpen((prev) => !prev)}
className="outline-none"
className="outline-none flex items-center justify-center"
disabled={disabled}
>
{label}
+3 -2
View File
@@ -2,7 +2,7 @@ import { useState, FC } from "react";
// ui
import { Button } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// types
import { IExportData } from "@plane/types";
@@ -18,7 +18,8 @@ export const SingleExport: FC<Props> = ({ service, refreshing }) => {
const checkExpiry = (inputDateString: string) => {
const currentDate = new Date();
const expiryDate = new Date(inputDateString);
const expiryDate = getDate(inputDateString);
if (!expiryDate) return false;
expiryDate.setDate(expiryDate.getDate() + 7);
return expiryDate > currentDate;
};
+2 -2
View File
@@ -64,9 +64,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
useGanttChart();
// rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
const renderBlockStructure = (view: ChartDataType, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: any) => ({
? blocks.map((block: IGanttBlock) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
+2 -2
View File
@@ -6,8 +6,8 @@ export interface IGanttBlock {
width: number;
};
sort_order: number;
start_date: Date | null;
target_date: Date | null;
start_date: Date | undefined;
target_date: Date | undefined;
}
export interface IBlockUpdateData {
@@ -1,4 +1,5 @@
// types
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { ChartDataType, IGanttBlock } from "../types";
// data
import { weeks, months } from "../data";
@@ -167,15 +168,16 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType,
const { startDate } = chartData.data;
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
if (!itemStartDate || !itemTargetDate) return null;
if (!itemStartDate || !itemTargetDate) return;
startDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0);
itemTargetDate.setHours(0, 0, 0, 0);
// position code starts
const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime();
const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)));
const positionDaysDifference = findTotalDaysInRange(startDate, itemStartDate, false);
if (!positionDaysDifference) return;
scrollPosition = positionDaysDifference * chartData.data.width;
var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12;
+1
View File
@@ -244,6 +244,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
/>
</FiltersDropdown>
+1
View File
@@ -248,6 +248,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
/>
</FiltersDropdown>
</div>
+19 -11
View File
@@ -21,6 +21,8 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
import { ISSUE_DELETED } from "constants/event-tracker";
//helpers
import { getDate } from "helpers/date-time.helper";
type TInboxIssueActionsHeader = {
workspaceSlug: string;
@@ -131,6 +133,8 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const handleInboxIssueNavigation = useCallback(
(direction: "next" | "prev") => {
if (!inboxIssues || !inboxIssueId) return;
const activeElement = document.activeElement as HTMLElement;
if (activeElement && (activeElement.classList.contains("tiptap") || activeElement.id === "title-input")) return;
const nextIssueIndex =
direction === "next"
? (currentIssueIndex + 1) % inboxIssues.length
@@ -169,11 +173,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const tomorrow = getDate(today);
tomorrow?.setDate(today.getDate() + 1);
useEffect(() => {
if (!issueStatus || !issueStatus.snoozed_till) return;
setDate(new Date(issueStatus.snoozed_till));
setDate(issueStatus.snoozed_till);
}, [issueStatus]);
if (!issueStatus || !issue || !inboxIssues) return <></>;
@@ -267,19 +271,23 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{({ close }) => (
<div className="flex h-full w-full flex-col gap-y-1">
<DayPicker
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
selected={getDate(date)}
defaultMonth={getDate(date)}
onSelect={(date) => {
if (!date) return;
setDate(date);
}}
mode="single"
className="border border-custom-border-200 rounded-md p-3"
disabled={[
{
before: tomorrow,
},
]}
disabled={
tomorrow
? [
{
before: tomorrow,
},
]
: undefined
}
/>
<Button
variant="primary"
@@ -287,7 +295,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
close();
inboxIssueOperations.updateInboxIssueStatus({
status: 0,
snoozed_till: new Date(date),
snoozed_till: date,
});
}}
>
+2 -2
View File
@@ -28,7 +28,7 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
if (!inboxIssueStatusDetail) return <></>;
const isSnoozedDatePassed =
inboxIssueDetail.status === 0 && new Date(inboxIssueDetail.snoozed_till ?? "") < new Date();
inboxIssueDetail.status === 0 && !!inboxIssueDetail.snoozed_till && inboxIssueDetail.snoozed_till < new Date();
return (
<div
@@ -46,7 +46,7 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
workspaceSlug,
projectId,
inboxIssueDetail.duplicate_to ?? "",
new Date(inboxIssueDetail.snoozed_till ?? "")
inboxIssueDetail.snoozed_till
)
) : (
<span>{inboxIssueStatusDetail.title}</span>
@@ -50,7 +50,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
buttonVariant="transparent-with-text"
className="w-full group"
buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
buttonClassName={`text-sm justify-between ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
placeholder="No cycle"
hideIcon
dropdownArrow
@@ -9,7 +9,7 @@ import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "c
// icons
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
// helper
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
type Props = {
workspaceSlug: string;
@@ -33,7 +33,7 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
const projectDetails = issue ? getProjectById(issue.project_id) : null;
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
@@ -74,7 +74,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
return (
<div
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && !isEmpty) {
if (e.key === "Enter" && !e.shiftKey && !isEmpty && !isSubmitting) {
handleSubmit(onSubmit)(e);
}
}}
@@ -11,7 +11,8 @@ import {
XCircle,
CircleDot,
CopyPlus,
CalendarDays,
CalendarClock,
CalendarCheck2,
} from "lucide-react";
// hooks
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
@@ -27,12 +28,13 @@ import {
IssueLabel,
ArchiveIssueModal,
} from "components/issues";
// components
import { IssueSubscription } from "./subscription";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// icons
import { ArchiveIcon, ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, Tooltip, UserGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { cn } from "helpers/common.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
@@ -98,10 +100,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const isInArchivableGroup =
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = issue.target_date ? getDate(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
return (
@@ -236,7 +238,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<span>Start date</span>
</div>
<DateDropdown
@@ -262,7 +264,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<span>Due date</span>
</div>
<DateDropdown
@@ -12,6 +12,8 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssuesFilter } from "store/issue/project-views";
// helpers
import { getDate } from "helpers/date-time.helper";
interface Props {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
@@ -47,8 +49,10 @@ export const CalendarMonthsDropdown: React.FC<Props> = observer((props: Props) =
const daysList = Object.keys(allDaysOfActiveWeek);
const firstDay = new Date(daysList[0]);
const lastDay = new Date(daysList[daysList.length - 1]);
const firstDay = getDate(daysList[0]);
const lastDay = getDate(daysList[daysList.length - 1]);
if (!firstDay || !lastDay) return "Week view";
if (firstDay.getMonth() === lastDay.getMonth() && firstDay.getFullYear() === lastDay.getFullYear())
return `${MONTHS_LIST[firstDay.getMonth() + 1].title} ${firstDay.getFullYear()}`;
@@ -110,7 +110,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
<div className="flex-shrink-0 text-xs text-custom-text-300">
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<div className="truncate text-xs">{issue.name}</div>
</Tooltip>
</div>
@@ -0,0 +1,48 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// hooks
import { useCycle } from "hooks/store";
// ui
import { CycleGroupIcon } from "@plane/ui";
// types
import { TCycleGroups } from "@plane/types";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedCycleFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
// store hooks
const { getCycleById } = useCycle();
return (
<>
{values.map((cycleId) => {
const cycleDetails = getCycleById(cycleId) ?? null;
if (!cycleDetails) return null;
const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups;
return (
<div key={cycleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3 w-3 flex-shrink-0" />
<span className="normal-case">{cycleDetails.name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(cycleId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});
@@ -1,12 +1,15 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
import { useRouter } from "next/router";
// hooks
import { useUser } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import {
AppliedCycleFilters,
AppliedDateFilters,
AppliedLabelsFilters,
AppliedMembersFilters,
AppliedModuleFilters,
AppliedPriorityFilters,
AppliedProjectFilters,
AppliedStateFilters,
@@ -34,6 +37,9 @@ const dateFilters = ["start_date", "target_date"];
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props;
// store hooks
const {
router: { moduleId, cycleId },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
@@ -104,6 +110,20 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
values={value}
/>
)}
{filterKey === "cycle" && !cycleId && (
<AppliedCycleFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("cycle", val)}
values={value}
/>
)}
{filterKey === "module" && !moduleId && (
<AppliedModuleFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("module", val)}
values={value}
/>
)}
{isEditingAllowed && (
<button
type="button"
@@ -5,5 +5,7 @@ export * from "./label";
export * from "./members";
export * from "./priority";
export * from "./project";
export * from "./module";
export * from "./cycle";
export * from "./state";
export * from "./state-group";
@@ -0,0 +1,44 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// hooks
import { useModule } from "hooks/store";
// ui
import { DiceIcon } from "@plane/ui";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedModuleFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
// store hooks
const { getModuleById } = useModule();
return (
<>
{values.map((moduleId) => {
const moduleDetails = getModuleById(moduleId) ?? null;
if (!moduleDetails) return null;
return (
<div key={moduleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="normal-case">{moduleDetails.name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(moduleId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});
@@ -11,7 +11,7 @@ import {
FilterSubGroupBy,
} from "components/issues";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types";
import { ILayoutDisplayFiltersOptions } from "constants/issue";
type Props = {
@@ -20,6 +20,7 @@ type Props = {
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
};
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
@@ -29,6 +30,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
handleDisplayFiltersUpdate,
handleDisplayPropertiesUpdate,
layoutDisplayFiltersOptions,
ignoreGroupedFilters = [],
} = props;
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
@@ -54,6 +56,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
group_by: val,
})
}
ignoreGroupedFilters={ignoreGroupedFilters}
/>
</div>
)}
@@ -71,6 +74,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
})
}
subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []}
ignoreGroupedFilters={ignoreGroupedFilters}
/>
</div>
)}
@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
@@ -12,10 +11,11 @@ type Props = {
displayFilters: IIssueDisplayFilterOptions;
groupByOptions: TIssueGroupByOptions[];
handleUpdate: (val: TIssueGroupByOptions) => void;
ignoreGroupedFilters: Partial<TIssueGroupByOptions>[];
};
export const FilterGroupBy: React.FC<Props> = observer((props) => {
const { displayFilters, groupByOptions, handleUpdate } = props;
const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
@@ -34,6 +34,7 @@ export const FilterGroupBy: React.FC<Props> = observer((props) => {
{ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => {
if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy)
return null;
if (ignoreGroupedFilters.includes(groupBy?.key)) return null;
return (
<FilterOption
@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
@@ -12,10 +11,11 @@ type Props = {
displayFilters: IIssueDisplayFilterOptions;
handleUpdate: (val: TIssueGroupByOptions) => void;
subGroupByOptions: TIssueGroupByOptions[];
ignoreGroupedFilters: Partial<TIssueGroupByOptions>[];
};
export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
const { displayFilters, handleUpdate, subGroupByOptions } = props;
const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
@@ -33,6 +33,7 @@ export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
<div>
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => {
if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null;
if (ignoreGroupedFilters.includes(subGroupBy?.key)) return null;
return (
<FilterOption
@@ -0,0 +1,96 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import sortBy from "lodash/sortBy";
// components
import { FilterHeader, FilterOption } from "components/issues";
import { useApplication, useCycle } from "hooks/store";
// ui
import { Loader, CycleGroupIcon } from "@plane/ui";
// types
import { TCycleGroups } from "@plane/types";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterCycle: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const {
router: { projectId },
} = useApplication();
const { getCycleById, getProjectCycleIds } = useCycle();
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined;
const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = sortBy(
cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())),
(cycle) => cycle.name.toLowerCase()
);
const handleViewToggle = () => {
if (!filteredOptions) return;
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
else setItemsToRender(filteredOptions.length);
};
const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups;
return (
<>
<FilterHeader
title={`Cycle ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((cycle) => (
<FilterOption
key={cycle.id}
isChecked={appliedFilters?.includes(cycle.id) ? true : false}
onClick={() => handleUpdate(cycle.id)}
icon={
<CycleGroupIcon cycleGroup={cycleStatus(cycle?.status)} className="h-3.5 w-3.5 flex-shrink-0" />
}
title={cycle.name}
activePulse={cycleStatus(cycle?.status) === "current" ? true : false}
/>
))}
{filteredOptions.length > 5 && (
<button
type="button"
className="ml-8 text-xs font-medium text-custom-primary-100"
onClick={handleViewToggle}
>
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
</button>
)}
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)
) : (
<Loader className="space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div>
)}
</>
);
});
@@ -1,6 +1,8 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
// components
import {
FilterAssignees,
@@ -13,6 +15,8 @@ import {
FilterState,
FilterStateGroup,
FilterTargetDate,
FilterCycle,
FilterModule,
} from "components/issues";
// types
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
@@ -30,6 +34,10 @@ type Props = {
export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
// hooks
const {
router: { moduleId, cycleId },
} = useApplication();
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
@@ -102,6 +110,28 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
</div>
)}
{/* cycle */}
{isFilterEnabled("cycle") && !cycleId && (
<div className="py-2">
<FilterCycle
appliedFilters={filters.cycle ?? null}
handleUpdate={(val) => handleFiltersUpdate("cycle", val)}
searchQuery={filtersSearchQuery}
/>
</div>
)}
{/* module */}
{isFilterEnabled("module") && !moduleId && (
<div className="py-2">
<FilterModule
appliedFilters={filters.module ?? null}
handleUpdate={(val) => handleFiltersUpdate("module", val)}
searchQuery={filtersSearchQuery}
/>
</div>
)}
{/* assignees */}
{isFilterEnabled("mentions") && (
<div className="py-2">
@@ -8,4 +8,6 @@ export * from "./project";
export * from "./start-date";
export * from "./state-group";
export * from "./state";
export * from "./cycle";
export * from "./module";
export * from "./target-date";
@@ -0,0 +1,89 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import sortBy from "lodash/sortBy";
// components
import { FilterHeader, FilterOption } from "components/issues";
import { useApplication, useModule } from "hooks/store";
// ui
import { Loader, DiceIcon } from "@plane/ui";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterModule: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks
const {
router: { projectId },
} = useApplication();
const { getModuleById, getProjectModuleIds } = useModule();
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined;
const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = sortBy(
modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())),
(module) => module.name.toLowerCase()
);
const handleViewToggle = () => {
if (!filteredOptions) return;
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
else setItemsToRender(filteredOptions.length);
};
return (
<>
<FilterHeader
title={`Module ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((cycle) => (
<FilterOption
key={cycle.id}
isChecked={appliedFilters?.includes(cycle.id) ? true : false}
onClick={() => handleUpdate(cycle.id)}
icon={<DiceIcon className="h-3 w-3 flex-shrink-0" />}
title={cycle.name}
/>
))}
{filteredOptions.length > 5 && (
<button
type="button"
className="ml-8 text-xs font-medium text-custom-primary-100"
onClick={handleViewToggle}
>
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
</button>
)}
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)
) : (
<Loader className="space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div>
)}
</>
);
});
@@ -8,10 +8,11 @@ type Props = {
title: React.ReactNode;
onClick?: () => void;
multiple?: boolean;
activePulse?: boolean;
};
export const FilterOption: React.FC<Props> = (props) => {
const { icon, isChecked, multiple = true, onClick, title } = props;
const { icon, isChecked, multiple = true, onClick, title, activePulse = false } = props;
return (
<button
@@ -30,6 +31,9 @@ export const FilterOption: React.FC<Props> = (props) => {
{icon && <div className="grid w-5 flex-shrink-0 place-items-center">{icon}</div>}
<div className="flex-grow truncate text-xs text-custom-text-200">{title}</div>
</div>
{activePulse && (
<div className="flex-shrink-0 text-xs w-2 h-2 rounded-full bg-custom-primary-100 animate-pulse ml-auto" />
)}
</button>
);
};
@@ -97,7 +97,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
<div className="flex-shrink-0 text-xs text-custom-text-300">
{projectIdentifier} {issueDetails?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issueDetails?.name}>
<Tooltip tooltipContent={issueDetails?.name}>
<span className="flex-grow truncate text-sm font-medium">{issueDetails?.name}</span>
</Tooltip>
</div>
@@ -291,27 +291,29 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</Droppable>
</div>
<KanBanView
issuesMap={issueMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters}
kanbanFilters={kanbanFilters}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
quickAddCallback={issues?.quickAddIssue}
viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
canEditProperties={canEditProperties}
storeType={storeType}
addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
<div className="w-max h-max">
<KanBanView
issuesMap={issueMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters}
kanbanFilters={kanbanFilters}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
quickAddCallback={issues?.quickAddIssue}
viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
canEditProperties={canEditProperties}
storeType={storeType}
addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
</div>
</DragDropContext>
</div>
</div>
@@ -71,7 +71,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
</WithDisplayPropertiesHOC>
{issue?.is_draft ? (
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
) : (
@@ -84,7 +84,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
disabled={!!issue?.tempId}
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
@@ -141,7 +141,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
>
<div
className={cn(
"rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
"rounded border-[0.5px] w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
{ "hover:cursor-grab": !isDragDisabled },
{ "border-custom-primary-100": snapshot.isDragging },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
@@ -1,6 +1,15 @@
import { observer } from "mobx-react-lite";
// hooks
import { useIssueDetail, useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
import {
useCycle,
useIssueDetail,
useKanbanView,
useLabel,
useMember,
useModule,
useProject,
useProjectState,
} from "hooks/store";
// components
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
@@ -49,6 +58,7 @@ export interface IGroupByKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@@ -74,36 +84,61 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props;
const member = useMember();
const project = useProject();
const label = useLabel();
const cycle = useCycle();
const _module = useModule();
const projectState = useProjectState();
const { peekIssue } = useIssueDetail();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member);
if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0);
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) =>
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
if (sub_group_by) {
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
} else {
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
else groupVisibility.showGroup = false;
}
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
return groupVisibility;
}
};
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className={`relative w-full flex gap-3 ${sub_group_by ? "h-full" : "h-full"}`}>
{groupList &&
groupList.length > 0 &&
groupList.map((_list: IGroupByColumn) => {
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list);
if (groupByVisibilityToggle.showGroup === false) return <></>;
return (
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
<div
className={`relative flex flex-shrink-0 flex-col group ${
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
} `}
>
{sub_group_by === null && (
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
<HeaderGroupByCard
@@ -123,7 +158,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div>
)}
{!groupByVisibilityToggle && (
{groupByVisibilityToggle.showIssues && (
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
@@ -141,7 +176,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
@@ -179,6 +213,7 @@ export interface IKanBan {
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
@@ -203,6 +238,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup,
subGroupIssueHeaderCount,
} = props;
const issueKanBanView = useKanbanView();
@@ -230,6 +266,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/>
);
});
@@ -106,13 +106,21 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
{icon ? icon : <Circle width={14} strokeWidth={2} />}
</div>
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `w-full flex-row`}`}>
<div
className={`relative overflow-hidden flex items-center gap-1 ${
verticalAlignPosition ? `flex-col` : `w-full flex-row`
}`}
>
<div
className={`line-clamp-1 font-medium text-custom-text-100 ${verticalAlignPosition ? `vertical-lr` : ``}`}
className={`inline-block truncate line-clamp-1 font-medium text-custom-text-100 overflow-hidden ${
verticalAlignPosition ? `vertical-lr max-h-[400px]` : ``
}`}
>
{title}
</div>
<div className={`text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}>
<div
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
>
{count || 0}
</div>
</div>
@@ -37,7 +37,7 @@ interface IKanbanGroup {
viewId?: string;
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean;
groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
@@ -80,6 +80,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
preloadedData = { ...preloadedData, state_id: groupValue };
} else if (groupByKey === "priority") {
preloadedData = { ...preloadedData, priority: groupValue };
} else if (groupByKey === "cycle") {
preloadedData = { ...preloadedData, cycle_id: groupValue };
} else if (groupByKey === "module") {
preloadedData = { ...preloadedData, module_ids: [groupValue] };
} else if (groupByKey === "labels" && groupValue != "None") {
preloadedData = { ...preloadedData, label_ids: [groupValue] };
} else if (groupByKey === "assignees" && groupValue != "None") {
@@ -96,6 +100,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
preloadedData = { ...preloadedData, state_id: subGroupValue };
} else if (subGroupByKey === "priority") {
preloadedData = { ...preloadedData, priority: subGroupValue };
} else if (groupByKey === "cycle") {
preloadedData = { ...preloadedData, cycle_id: subGroupValue };
} else if (groupByKey === "module") {
preloadedData = { ...preloadedData, module_ids: [subGroupValue] };
} else if (subGroupByKey === "labels" && subGroupValue != "None") {
preloadedData = { ...preloadedData, label_ids: [subGroupValue] };
} else if (subGroupByKey === "assignees" && subGroupValue != "None") {
@@ -115,9 +123,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
{(provided: any, snapshot: any) => (
<div
className={`relative h-full w-full transition-all ${
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
}`}
className={`relative h-full transition-all ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
@@ -18,7 +18,7 @@ import {
} from "@plane/types";
// constants
import { EIssueActions } from "../types";
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
@@ -29,7 +29,33 @@ interface ISubGroupSwimlaneHeader {
list: IGroupByColumn[];
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
}
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
let headerCount = 0;
Object.keys(issueIds).map((groupState) => {
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0);
});
return headerCount;
};
const visibilitySubGroupByGroupCount = (
issueIds: TSubGroupedIssues,
_list: IGroupByColumn,
showEmptyGroup: boolean
): boolean => {
let subGroupHeaderVisibility = true;
if (showEmptyGroup) subGroupHeaderVisibility = true;
else {
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
else subGroupHeaderVisibility = false;
}
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds,
sub_group_by,
@@ -37,25 +63,36 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
list,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
}) => (
<div className="relative flex h-max min-h-full w-full items-center">
<div className="relative flex gap-2 h-max min-h-full w-full items-center">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[340px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
/>
</div>
))}
list.map((_list: IGroupByColumn) => {
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
issueIds as TSubGroupedIssues,
_list,
showEmptyGroup
);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
/>
</div>
);
})}
</div>
);
@@ -115,52 +152,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
return issueCount;
};
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const subGroupVisibility = {
showGroup: true,
showIssues: true,
};
if (showEmptyGroup) subGroupVisibility.showGroup = true;
else {
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
else subGroupVisibility.showGroup = false;
}
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
return subGroupVisibility;
};
return (
<div className="relative h-max min-h-full w-full">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
list.map((_list: any) => {
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
return (
<div className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
)}
</div>
))}
{subGroupByVisibilityToggle.showIssues && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}
/>
</div>
)}
</div>
);
})}
</div>
);
});
@@ -217,10 +276,28 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const member = useMember();
const project = useProject();
const label = useLabel();
const cycle = useCycle();
const _module = useModule();
const projectState = useProjectState();
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member);
const groupByList = getGroupByColumns(
group_by as GroupByColumnTypes,
project,
cycle,
_module,
label,
projectState,
member
);
const subGroupByList = getGroupByColumns(
sub_group_by as GroupByColumnTypes,
project,
cycle,
_module,
label,
projectState,
member
);
if (!groupByList || !subGroupByList) return null;
@@ -234,6 +311,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
list={groupByList}
showEmptyGroup={showEmptyGroup}
/>
</div>
@@ -65,7 +65,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
)}
{issue?.is_draft ? (
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
) : (
@@ -78,7 +78,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
disabled={!!issue?.tempId}
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
@@ -3,7 +3,7 @@ import { useRef } from "react";
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
import { HeaderGroupByCard } from "./headers/group-by-card";
// hooks
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store";
// types
import {
GroupByColumnTypes,
@@ -65,10 +65,21 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const project = useProject();
const label = useLabel();
const projectState = useProjectState();
const cycle = useCycle();
const _module = useModule();
const containerRef = useRef<HTMLDivElement | null>(null);
const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
const groups = getGroupByColumns(
group_by as GroupByColumnTypes,
project,
cycle,
_module,
label,
projectState,
member,
true
);
if (!groups) return null;
@@ -1,7 +1,7 @@
import { useCallback, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Layers, Link, Paperclip } from "lucide-react";
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
import xor from "lodash/xor";
// hooks
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store";
@@ -19,7 +19,7 @@ import {
StateDropdown,
} from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { cn } from "helpers/common.helper";
// types
@@ -229,10 +229,10 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
@@ -240,7 +240,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* basic properties */}
{/* state */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
<div className="h-5">
<div className="h-5 truncate">
<StateDropdown
value={issue.state_id}
onChange={handleState}
@@ -284,8 +284,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue.start_date ?? null}
onChange={handleStartDate}
maxDate={maxDate ?? undefined}
maxDate={maxDate}
placeholder="Start date"
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
showTooltip
@@ -299,8 +300,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue?.target_date ?? null}
onChange={handleTargetDate}
minDate={minDate ?? undefined}
minDate={minDate}
placeholder="Due date"
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""}
clearIconClassName="!text-custom-text-100"
@@ -96,6 +96,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
storeType={EIssuesStoreType.PROJECT}
/>
<CustomMenu
menuItemsClassName="z-[14]"
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
@@ -56,6 +56,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
onSubmit={handleDelete}
/>
<CustomMenu
menuItemsClassName="z-[14]"
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
@@ -106,6 +106,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
storeType={EIssuesStoreType.CYCLE}
/>
<CustomMenu
menuItemsClassName="z-[14]"
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
@@ -106,6 +106,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
storeType={EIssuesStoreType.MODULE}
/>
<CustomMenu
menuItemsClassName="z-[14]"
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
@@ -108,6 +108,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
isDraft={isDraftIssue}
/>
<CustomMenu
menuItemsClassName="z-[14]"
placement="bottom-start"
customButton={customActionButton}
portalElement={portalElement}
@@ -1,13 +1,14 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { CalendarCheck2 } from "lucide-react";
// hooks
import { useProjectState } from "hooks/store";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { cn } from "helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
// types
import { TIssue } from "@plane/types";
@@ -29,7 +30,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.target_date}
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
minDate={getDate(issue.start_date)}
onChange={(data) => {
const targetDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
@@ -43,6 +44,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
}}
disabled={disabled}
placeholder="Due date"
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant="transparent-with-text"
buttonContainerClassName="w-full"
buttonClassName={cn("rounded-none text-left", {
@@ -1,9 +1,10 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { CalendarClock } from "lucide-react";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
@@ -21,7 +22,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.start_date}
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
maxDate={getDate(issue.target_date)}
onChange={(data) => {
const startDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
@@ -35,6 +36,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
}}
disabled={disabled}
placeholder="Start date"
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
@@ -189,7 +189,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
<td
id={`issue-${issueDetail.id}`}
className={cn(
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-[1] border-custom-border-200",
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
{
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
},
@@ -241,9 +241,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
disabled={!!issueDetail?.tempId}
>
<div className="w-full overflow-hidden">
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
<Tooltip tooltipContent={issueDetail.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
tabIndex={-1}
>
{issueDetail.name}
@@ -19,10 +19,10 @@ export const SpreadsheetHeader = (props: Props) => {
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props;
return (
<thead className="sticky top-0 left-0 z-[2] border-b-[0.5px] border-custom-border-100">
<thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100">
<tr>
<th
className="sticky left-0 z-[2] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
className="sticky left-0 z-[15] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
tabIndex={-1}
>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
+80 -5
View File
@@ -1,16 +1,25 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
// stores
import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";
import { ICycleStore } from "store/cycle.store";
import { IModuleStore } from "store/module.store";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { STATE_GROUPS } from "constants/state";
import { ISSUE_PRIORITIES } from "constants/issue";
// types
import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types";
import { ContrastIcon } from "lucide-react";
export const getGroupByColumns = (
groupBy: GroupByColumnTypes | null,
project: IProjectStore,
cycle: ICycleStore,
module: IModuleStore,
label: ILabelStore,
projectState: IStateStore,
member: IMemberRootStore,
@@ -19,6 +28,10 @@ export const getGroupByColumns = (
switch (groupBy) {
case "project":
return getProjectColumns(project);
case "cycle":
return getCycleColumns(project, cycle);
case "module":
return getModuleColumns(project, module);
case "state":
return getStateColumns(projectState);
case "state_detail.group":
@@ -55,6 +68,68 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
}) as any;
};
const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => {
const { currentProjectDetails } = projectStore;
const { getProjectCycleIds, getCycleById } = cycleStore;
if (!currentProjectDetails || !currentProjectDetails?.id) return;
const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined;
if (!cycleIds) return;
const cycles = [];
cycleIds.map((cycleId) => {
const cycle = getCycleById(cycleId);
if (cycle) {
const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft";
cycles.push({
id: cycle.id,
name: cycle.name,
icon: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
payload: { cycle_id: cycle.id },
});
}
});
cycles.push({
id: "None",
name: "None",
icon: <ContrastIcon className="h-3.5 w-3.5" />,
});
return cycles as any;
};
const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => {
const { currentProjectDetails } = projectStore;
const { getProjectModuleIds, getModuleById } = moduleStore;
if (!currentProjectDetails || !currentProjectDetails?.id) return;
const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined;
if (!moduleIds) return;
const modules = [];
moduleIds.map((moduleId) => {
const _module = getModuleById(moduleId);
if (_module)
modules.push({
id: _module.id,
name: _module.name,
icon: <DiceIcon className="w-3.5 h-3.5" />,
payload: { module_ids: [_module.id] },
});
}) as any;
modules.push({
id: "None",
name: "None",
icon: <DiceIcon className="w-3.5 h-3.5" />,
});
return modules as any;
};
const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => {
const { projectStates } = projectState;
if (!projectStates) return;
+4 -4
View File
@@ -26,10 +26,10 @@ import {
MemberDropdown,
StateDropdown,
} from "components/dropdowns";
// ui
//ui
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import type { TIssue, ISearchIssueResponse } from "@plane/types";
@@ -236,10 +236,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
const minDate = getDate(startDate);
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
const maxDate = getDate(targetDate);
maxDate?.setDate(maxDate.getDate());
const projectDetails = getProjectById(projectId);
+2 -1
View File
@@ -100,8 +100,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const fetchIssueDetail = async (issueId: string | undefined) => {
if (!workspaceSlug) return;
if (!projectId || issueId === undefined) {
setDescription("<p></p>");
setDescription(data?.description_html || "<p></p>");
return;
}
const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT");
+142 -142
View File
@@ -14,153 +14,153 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { ProjectAnalyticsModal } from "components/analytics";
export const IssuesMobileHeader = () => {
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const [analyticsModal, setAnalyticsModal] = useState(false);
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
};
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const [analyticsModal, setAnalyticsModal] = useState(false);
const { workspaceSlug, projectId } = router.query as {
workspaceSlug: string;
projectId: string;
};
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
const {
project: { projectMemberIds },
} = useMember();
const activeLayout = issueFilters?.displayFilters?.layout;
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
const {
project: { projectMemberIds },
} = useMember();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</>
);
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</>
);
};
@@ -1,6 +1,16 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react";
import {
Signal,
Tag,
Triangle,
LayoutPanelTop,
CircleDot,
CopyPlus,
XCircle,
CalendarClock,
CalendarCheck2,
} from "lucide-react";
// hooks
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
// ui icons
@@ -16,9 +26,9 @@ import {
} from "components/issues";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// components
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// helpers
import { cn } from "helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
interface IPeekOverviewProperties {
@@ -44,10 +54,10 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const isEstimateEnabled = projectDetails?.estimate;
const stateDetails = getStateById(issue.state_id);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
@@ -118,7 +128,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* start date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<CalendarClock className="h-4 w-4 flex-shrink-0" />
<span>Start date</span>
</div>
<DateDropdown
@@ -145,7 +155,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
{/* due date */}
<div className="flex w-full items-center gap-3 h-8">
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<span>Due date</span>
</div>
<DateDropdown
@@ -117,7 +117,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<Tooltip tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
+3 -3
View File
@@ -6,7 +6,7 @@ import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/d
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IModule } from "@plane/types";
@@ -147,8 +147,8 @@ export const ModuleForm: React.FC<Props> = (props) => {
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -7,6 +7,7 @@ import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components
import { ModuleGanttBlock } from "components/modules";
// types
import { IModule } from "@plane/types";
import { getDate } from "helpers/date-time.helper";
export const ModulesListGanttChartView: React.FC = observer(() => {
// router
@@ -32,8 +33,8 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
start_date: getDate(block.start_date),
target_date: getDate(block.target_date),
};
});
+3 -3
View File
@@ -12,7 +12,7 @@ import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// constants
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
@@ -135,8 +135,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const completionPercentage = (moduleDetails.completed_issues / moduleTotalIssues) * 100;
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(moduleDetails.start_date ?? "");
const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const isDateValid = moduleDetails.target_date || moduleDetails.start_date;
+3 -3
View File
@@ -12,7 +12,7 @@ import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
import { Avatar, AvatarGroup, CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// constants
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
@@ -129,8 +129,8 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
const completionPercentage =
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(moduleDetails.start_date ?? "");
const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const renderDate = moduleDetails.start_date || moduleDetails.target_date;
+144 -143
View File
@@ -9,154 +9,155 @@ import router from "next/router";
import { useCallback, useState } from "react";
export const ModuleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const { getModuleById } = useModule();
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const [analyticsModal, setAnalyticsModal] = useState(false);
const { getModuleById } = useModule();
const layouts = [
{ key: "list", title: "List", icon: List },
{ key: "kanban", title: "Kanban", icon: Kanban },
{ key: "calendar", title: "Calendar", icon: Calendar },
];
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const activeLayout = issueFilters?.displayFilters?.layout;
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const activeLayout = issueFilters?.displayFilters?.layout;
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
return (
<div className="block md:hidden">
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
return (
<div className="block md:hidden">
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
<div className="flex justify-evenly py-2 border-b border-custom-border-200">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-custom-text-200 text-sm">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{layouts.map((layout, index) => (
<CustomMenu.MenuItem
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="w-3 h-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Filters
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</FiltersDropdown>
</div>
);
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-custom-text-200 text-sm">
Display
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
/>
</FiltersDropdown>
</div>
<button
onClick={() => setAnalyticsModal(true)}
className="flex flex-grow justify-center text-custom-text-200 text-sm border-l border-custom-border-200"
>
Analytics
</button>
</div>
</div>
);
};
+29 -29
View File
@@ -25,7 +25,7 @@ import { DateRangeDropdown, MemberDropdown } from "components/dropdowns";
// ui
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
@@ -201,8 +201,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
});
}, [moduleDetails, reset]);
const isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`);
const startDate = getDate(moduleDetails?.start_date);
const endDate = getDate(moduleDetails?.target_date);
const isStartValid = startDate && startDate <= new Date();
const isEndValid = startDate && endDate && endDate >= startDate;
const progressPercentage = moduleDetails
? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
@@ -342,38 +344,36 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Controller
control={control}
name="target_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "Target date",
}}
/>
)}
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => {
const startDate = getDate(startDateValue);
const endDate = getDate(endDateValue);
return (
<DateRangeDropdown
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDate,
to: endDate,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "Target date",
}}
/>
);
}}
/>
)}
/>
</div>
</div>
{moduleDetails.description && (
<span className="w-full whitespace-normal break-words py-2.5 text-sm leading-5 text-custom-text-200">
{moduleDetails.description}
</span>
)}
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
@@ -13,7 +13,7 @@ import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
import { snoozeOptions } from "constants/notification";
// helper
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "helpers/date-time.helper";
// type
import type { IUserNotification, NotificationType } from "@plane/types";
// constants
@@ -119,7 +119,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const notificationField = notification.data.issue_activity.field;
const notificationTriggeredBy = notification.triggered_by_details;
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
const snoozedTillDate = getDate(notification?.snoozed_till);
if (snoozedTillDate && isSnoozedTabOpen && snoozedTillDate < new Date()) return null;
return (
<Link
@@ -12,6 +12,8 @@ import useToast from "hooks/use-toast";
import { Button, CustomSelect } from "@plane/ui";
// types
import type { IUserNotification } from "@plane/types";
// helpers
import { getDate } from "helpers/date-time.helper";
type SnoozeModalProps = {
isOpen: boolean;
@@ -60,7 +62,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
if (!formDataDate) return timeStamps;
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
const isToday = today.toDateString() === getDate(formDataDate)?.toDateString();
if (!isToday) return timeStamps;
@@ -93,9 +95,9 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
);
const minutes = parseInt(time[1]);
const dateTime = new Date(formData.date);
dateTime.setHours(hours);
dateTime.setMinutes(minutes);
const dateTime: Date | undefined = getDate(formData?.date);
dateTime?.setHours(hours);
dateTime?.setMinutes(minutes);
await handleSubmitSnooze(notification.id, dateTime).then(() => {
handleClose();
@@ -214,10 +216,11 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "AM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "AM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
AM
</div>
@@ -225,10 +228,11 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "PM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
PM
</div>
+1 -1
View File
@@ -144,7 +144,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const handleProjectClick = () => {
if (window.innerWidth < 768) {
themeStore.toggleMobileSidebar();
themeStore.toggleSidebar();
}
};

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