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
Henit Chobisa 59c9b3bdce chore: added auto merge CI for merging sync branches 2024-03-04 15:13:39 +05:30
65 changed files with 1110 additions and 484 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
+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>
@@ -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 -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;
@@ -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 });
}
+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>
+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
+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);
}}
+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;
+17 -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;
@@ -171,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 <></>;
@@ -269,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"
@@ -289,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>
@@ -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);
@@ -28,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";
@@ -99,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 (
@@ -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>
@@ -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>
@@ -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>
@@ -58,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) => {
@@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props;
const member = useMember();
@@ -97,17 +99,27 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
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) => {
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
if (sub_group_by) {
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
return false;
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
} else {
if (kanbanFilters?.group_by.includes(_list.id)) return true;
return false;
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;
}
};
@@ -115,13 +127,18 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
return (
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{groupList &&
groupList.length > 0 &&
groupList.map((_list: IGroupByColumn) => {
{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-[350px]`}`}>
<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
@@ -141,7 +158,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div>
)}
{!groupByVisibilityToggle && (
{groupByVisibilityToggle.showIssues && (
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
@@ -159,7 +176,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
@@ -197,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) => {
@@ -221,6 +238,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup,
subGroupIssueHeaderCount,
} = props;
const issueKanBanView = useKanbanView();
@@ -248,6 +266,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/>
);
});
@@ -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;
}
@@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
list: IGroupByColumn[];
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
}
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
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,
@@ -46,25 +63,36 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
list,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
}) => (
<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-[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>
))}
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>
);
@@ -124,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>
);
});
@@ -261,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>
@@ -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,7 +284,7 @@ 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"}
@@ -300,7 +300,7 @@ 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"}
@@ -6,9 +6,9 @@ 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";
@@ -30,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(
@@ -4,7 +4,7 @@ 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";
@@ -22,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(
@@ -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}
+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);
@@ -26,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 {
@@ -54,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 (
@@ -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;
+29 -23
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,26 +344,30 @@ 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",
}}
/>
);
}}
/>
)}
/>
@@ -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>
+6 -1
View File
@@ -8,7 +8,12 @@ export const INBOX_STATUS: {
status: number;
icon: LucideIcon;
title: string;
description: (workspaceSlug: string, projectId: string, issueId: string, snoozedTillDate: Date) => JSX.Element;
description: (
workspaceSlug: string,
projectId: string,
issueId: string,
snoozedTillDate: Date | undefined
) => JSX.Element;
textColor: (snoozeDatePassed: boolean) => string;
bgColor: (snoozeDatePassed: boolean) => string;
borderColor: (snoozeDatePassed: boolean) => string;
+2 -2
View File
@@ -91,7 +91,7 @@ const UserNotificationContextProvider: React.FC<{
const [state, dispatch] = useReducer(reducer, initialState);
const { selectedTab, snoozed, archived, readNotification, selectedNotificationForSnooze } = state;
const { selectedTab, snoozed, archived, readNotification } = state;
const params = {
type: snoozed || archived || readNotification ? undefined : selectedTab,
@@ -207,7 +207,7 @@ const UserNotificationContextProvider: React.FC<{
(previousNotifications: any) =>
previousNotifications?.map((notification: any) =>
notification.id === notificationId
? { ...notification, snoozed_till: isSnoozed ? null : new Date(dateTime!) }
? { ...notification, snoozed_till: isSnoozed ? null : dateTime }
: notification
) || [],
false
+48 -19
View File
@@ -1,5 +1,5 @@
import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns";
import { isNil } from "lodash";
import isNumber from "lodash/isNumber";
// Format Date Helpers
/**
@@ -8,10 +8,11 @@ import { isNil } from "lodash";
* @param {Date | string} date
* @example renderFormattedDate("2024-01-01") // Jan 01, 2024
*/
export const renderFormattedDate = (date: string | Date): string | null => {
if (!date) return null;
export const renderFormattedDate = (date: string | Date | undefined | null): string | null => {
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return null;
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return null; // Return null for invalid dates
// Format the date in format (MMM dd, yyyy)
@@ -26,9 +27,10 @@ export const renderFormattedDate = (date: string | Date): string | null => {
* @example renderShortDateFormat("2024-01-01") // Jan 01
*/
export const renderFormattedDateWithoutYear = (date: string | Date): string => {
if (!date) return "";
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return "";
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
// Format the date in short format (MMM dd)
@@ -43,9 +45,10 @@ export const renderFormattedDateWithoutYear = (date: string | Date): string => {
* @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01"
*/
export const renderFormattedPayloadDate = (date: Date | string): string | null => {
if (!date) return null;
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return null;
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return null; // Return null for invalid dates
// Format the date in payload format (yyyy-mm-dd)
@@ -62,10 +65,14 @@ export const renderFormattedPayloadDate = (date: Date | string): string | null =
* @example renderFormattedTime("2024-01-01 13:00:00") // 13:00
* @example renderFormattedTime("2024-01-01 13:00:00", "12-hour") // 01:00 PM
*/
export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => {
if (!date || date === "") return "";
export const renderFormattedTime = (
date: string | Date | undefined | null,
timeFormat: "12-hour" | "24-hour" = "24-hour"
): string => {
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return "";
// Check if the parsed date is valid
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
// Format the date in 12 hour format if in12HourFormat is true
@@ -92,10 +99,11 @@ export const findTotalDaysInRange = (
endDate: Date | string | undefined | null,
inclusive: boolean = true
): number | undefined => {
if (!startDate || !endDate) return undefined;
// Parse the dates to check if they are valid
const parsedStartDate = new Date(startDate);
const parsedEndDate = new Date(endDate);
const parsedStartDate = getDate(startDate);
const parsedEndDate = getDate(endDate);
// return if undefined
if (!parsedStartDate || !parsedEndDate) return;
// Check if the parsed dates are valid before calculating the difference
if (!isValid(parsedStartDate) || !isValid(parsedEndDate)) return 0; // Return 0 for invalid dates
// Calculate the difference in days
@@ -131,6 +139,7 @@ export const calculateTimeAgo = (time: string | number | Date | null): string =>
if (!time) return "";
// Parse the time to check if it is valid
const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time;
// return if undefined
if (!parsedTime) return ""; // Return empty string for invalid dates
// Format the time in the form of amount of time passed since the event happened
const distance = formatDistanceToNow(parsedTime, { addSuffix: true });
@@ -164,7 +173,7 @@ export const isDateGreaterThanToday = (dateStr: string): boolean => {
* @example getWeekNumber(new Date("2023-09-01")) // 35
*/
export const getWeekNumberOfDate = (date: Date): number => {
const currentDate = new Date(date);
const currentDate = date;
// Adjust the starting day to Sunday (0) instead of Monday (1)
const startDate = new Date(currentDate.getFullYear(), 0, 1);
// Calculate the number of days between currentDate and startDate
@@ -186,10 +195,30 @@ export const checkIfDatesAreEqual = (
date1: Date | string | null | undefined,
date2: Date | string | null | undefined
): boolean => {
if (isNil(date1) && isNil(date2)) return true;
if (isNil(date1) || isNil(date2)) return false;
const parsedDate1 = getDate(date1);
const parsedDate2 = getDate(date2);
// return if undefined
if (!parsedDate1 && !parsedDate2) return true;
if (!parsedDate1 || !parsedDate2) return false;
const parsedDate1 = new Date(date1);
const parsedDate2 = new Date(date2);
return isEqual(parsedDate1, parsedDate2);
};
/**
* 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);
};
+34
View File
@@ -1,5 +1,7 @@
// types
import { IIssueFilterOptions } from "@plane/types";
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import { getDate } from "./date-time.helper";
export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
filters && Object.keys(filters).length > 0
@@ -14,3 +16,35 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
.reduce((curr, prev) => curr + prev, 0)
: 0;
/**
* @description checks if the date satisfies the filter
* @param {Date} date
* @param {string} filter
* @returns {boolean}
*/
export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
const [value, operator, from] = filter.split(";");
const dateValue = getDate(value);
if (!from && dateValue) {
if (operator === "after") return date >= dateValue;
if (operator === "before") return date <= dateValue;
}
if (from === "fromnow") {
if (operator === "before") {
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) <= -7;
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) <= -14;
if (value === "1_months") return differenceInCalendarDays(date, new Date()) <= -30;
}
if (operator === "after") {
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) >= 7;
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) >= 14;
if (value === "1_months") return differenceInCalendarDays(date, new Date()) >= 30;
if (value === "2_months") return differenceInCalendarDays(date, new Date()) >= 60;
}
}
return false;
};
+6 -3
View File
@@ -1,6 +1,7 @@
import { v4 as uuidv4 } from "uuid";
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
// helpers
import { getDate } from "./date-time.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import {
@@ -157,7 +158,9 @@ export const shouldHighlightIssueDueDate = (
// if the issue is completed or cancelled, don't highlight the due date
if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false;
const parsedDate = new Date(date);
const parsedDate = getDate(date);
if (!parsedDate) return false;
const targetDateDistance = differenceInCalendarDays(parsedDate, new Date());
// if the issue is overdue, highlight the due date
@@ -168,6 +171,6 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
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),
}));
@@ -21,6 +21,7 @@ import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/doc
import { Spinner } from "@plane/ui";
// assets
// helpers
import { getDate } from "helpers/date-time.helper";
// types
import { IPage } from "@plane/types";
import { NextPageWithLayout } from "lib/types";
@@ -271,8 +272,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
@@ -282,7 +283,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: archived_at ? new Date(archived_at) : undefined,
archived_at: getDate(archived_at),
}
: undefined
}
@@ -298,8 +299,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
+11 -4
View File
@@ -4,6 +4,10 @@ import { isFuture, isPast, isToday } from "date-fns";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
// types
// helpers
import { getDate } from "helpers/date-time.helper";
// mobx
// services
import { ICycle, CycleDateCheckData } from "@plane/types";
// mobx
import { RootStore } from "store/root.store";
@@ -118,8 +122,9 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
const isEndDateToday = isToday(new Date(c.end_date ?? ""));
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
const isEndDateToday = endDate && isToday(endDate);
return c.project_id === projectId && hasEndDatePassed && !isEndDateToday;
});
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
@@ -134,7 +139,8 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
const startDate = getDate(c.start_date);
const isStartDateUpcoming = startDate && isFuture(startDate);
return c.project_id === projectId && isStartDateUpcoming;
});
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
@@ -149,7 +155,8 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
return c.project_id === projectId && !hasEndDatePassed;
});
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
+38 -35
View File
@@ -1,16 +1,17 @@
import orderBy from "lodash/orderBy";
import get from "lodash/get";
import indexOf from "lodash/indexOf";
import isEmpty from "lodash/isEmpty";
import orderBy from "lodash/orderBy";
import values from "lodash/values";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
import { IIssueRootStore } from "../root.store";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
import { STATE_GROUPS } from "constants/state";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
// store
import { IIssueRootStore } from "../root.store";
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
@@ -62,35 +63,35 @@ export class IssueHelperStore implements TIssueHelperStore {
issues: TIssueMap,
isCalendarIssues: boolean = false
) => {
const _issues: { [group_id: string]: string[] } = {};
if (!groupBy) return _issues;
const currentIssues: { [group_id: string]: string[] } = {};
if (!groupBy) return currentIssues;
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
_issues[group] = [];
currentIssues[group] = [];
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const _issue = projectIssues[issue];
const currentIssue = projectIssues[issue];
let groupArray = [];
if (groupBy === "state_detail.group") {
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
// if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap
const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None";
groupArray = [state_group];
} else {
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : [];
const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"];
}
for (const group of groupArray) {
if (group && _issues[group]) _issues[group].push(_issue.id);
else if (group) _issues[group] = [_issue.id];
if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id);
else if (group) currentIssues[group] = [currentIssue.id];
}
}
return _issues;
return currentIssues;
};
subGroupedIssues = (
@@ -99,45 +100,47 @@ export class IssueHelperStore implements TIssueHelperStore {
orderBy: TIssueOrderByOptions,
issues: TIssueMap
) => {
const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return _issues;
const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return currentIssues;
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => {
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => {
const groupByIssues: { [group_id: string]: string[] } = {};
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
groupByIssues[group] = [];
});
_issues[sub_group] = groupByIssues;
currentIssues[sub_group] = groupByIssues;
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const _issue = projectIssues[issue];
const currentIssue = projectIssues[issue];
let subGroupArray = [];
let groupArray = [];
if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") {
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None";
subGroupArray = [state_group];
groupArray = [state_group];
} else {
const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]);
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : [];
groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : [];
const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]);
const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"];
groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"];
}
for (const subGroup of subGroupArray) {
for (const group of groupArray) {
if (subGroup && group && _issues?.[subGroup]?.[group]) _issues[subGroup][group].push(_issue.id);
else if (subGroup && group && _issues[subGroup]) _issues[subGroup][group] = [_issue.id];
else if (subGroup && group) _issues[subGroup] = { [group]: [_issue.id] };
if (subGroup && group && currentIssues?.[subGroup]?.[group])
currentIssues[subGroup][group].push(currentIssue.id);
else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group] = [currentIssue.id];
else if (subGroup && group) currentIssues[subGroup] = { [group]: [currentIssue.id] };
}
}
}
return _issues;
return currentIssues;
};
unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) =>
@@ -215,8 +218,8 @@ export class IssueHelperStore implements TIssueHelperStore {
const moduleMap = this.rootStore?.moduleMap;
if (!moduleMap) break;
for (const dataId of dataIdsArray) {
const _module = moduleMap[dataId];
if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase());
const currentModule = moduleMap[dataId];
if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase());
}
break;
case "cycle_id":
@@ -233,10 +236,10 @@ export class IssueHelperStore implements TIssueHelperStore {
}
/**
* This Method is mainly used to filter out empty values in the begining
* This Method is mainly used to filter out empty values in the beginning
* @param key key of the value that is to be checked if empty
* @param object any object in which the key's value is to be checked
* @returns 1 if emoty, 0 if not empty
* @returns 1 if empty, 0 if not empty
*/
getSortOrderToFilterEmptyValues(key: string, object: any) {
const value = object?.[key];
@@ -388,7 +391,7 @@ export class IssueHelperStore implements TIssueHelperStore {
getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] {
if (!value || value === null || value === undefined) return ["None"];
if (Array.isArray(value))
if (value.length) return value;
if (value && value.length) return value;
else return ["None"];
else if (typeof value === "boolean") return [value ? "True" : "False"];
else if (typeof value === "number") return [value.toString()];
+7 -7
View File
@@ -10,7 +10,7 @@ export interface IPageStore {
access: number;
archived_at: string | null;
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html: string;
@@ -23,7 +23,7 @@ export interface IPageStore {
name: string;
owned_by: string;
project: string;
updated_at: Date;
updated_at: string | null;
updated_by: string;
workspace: string;
@@ -52,7 +52,7 @@ export class PageStore implements IPageStore {
isSubmitting: "submitting" | "submitted" | "saved" = "saved";
archived_at: string | null;
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html = "";
@@ -64,7 +64,7 @@ export class PageStore implements IPageStore {
name = "";
owned_by: string;
project: string;
updated_at: Date;
updated_at: string | null;
updated_by: string;
workspace: string;
oldName = "";
@@ -94,9 +94,9 @@ export class PageStore implements IPageStore {
cleanup: action,
});
this.created_by = page?.created_by || "";
this.created_at = page?.created_at || new Date();
this.created_at = page?.created_at ?? "";
this.color = page?.color || "";
this.archived_at = page?.archived_at || null;
this.archived_at = page?.archived_at ?? null;
this.name = page?.name || "";
this.description = page?.description || "";
this.description_stripped = page?.description_stripped || "";
@@ -104,7 +104,7 @@ export class PageStore implements IPageStore {
this.access = page?.access || 0;
this.workspace = page?.workspace || "";
this.updated_by = page?.updated_by || "";
this.updated_at = page?.updated_at || new Date();
this.updated_at = page?.updated_at ?? "";
this.project = page?.project || "";
this.owned_by = page?.owned_by || "";
this.labels = page?.labels || [];
+17 -18
View File
@@ -8,6 +8,7 @@ import { PageStore, IPageStore } from "store/page.store";
import { IPage, IRecentPages } from "@plane/types";
import { RootStore } from "./root.store";
import { isThisWeek, isToday, isYesterday } from "date-fns";
import { getDate } from "helpers/date-time.helper";
export interface IProjectPageStore {
loader: boolean;
@@ -73,8 +74,8 @@ export class ProjectPageStore implements IProjectPageStore {
const allProjectIds = Object.keys(this.projectPageMap[projectId]);
return allProjectIds.sort((a, b) => {
const dateA = new Date(this.projectPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectPageMap[projectId][b].created_at).getTime();
const dateA = getDate(this.projectPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
return dateB - dateA;
});
}
@@ -84,8 +85,8 @@ export class ProjectPageStore implements IProjectPageStore {
if (!projectId || !this.projectArchivedPageMap[projectId]) return [];
const archivedPages = Object.keys(this.projectArchivedPageMap[projectId]);
return archivedPages.sort((a, b) => {
const dateA = new Date(this.projectArchivedPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectArchivedPageMap[projectId][b].created_at).getTime();
const dateA = getDate(this.projectArchivedPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectArchivedPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
return dateB - dateA;
});
}
@@ -126,26 +127,24 @@ export class ProjectPageStore implements IProjectPageStore {
const projectId = this.rootStore.app.router.projectId;
if (!this.projectPageIds || !projectId) return;
const today: string[] = this.projectPageIds.filter((page) =>
isToday(new Date(this.projectPageMap[projectId][page].updated_at))
);
const today: string[] = this.projectPageIds.filter((page) => {
const updatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return updatedAt && isToday(updatedAt);
});
const yesterday: string[] = this.projectPageIds.filter((page) =>
isYesterday(new Date(this.projectPageMap[projectId][page].updated_at))
);
const yesterday: string[] = this.projectPageIds.filter((page) => {
const updatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return updatedAt && isYesterday(updatedAt);
});
const this_week: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return (
isThisWeek(new Date(pageUpdatedAt)) &&
!isToday(new Date(pageUpdatedAt)) &&
!isYesterday(new Date(pageUpdatedAt))
);
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && isThisWeek(pageUpdatedAt) && !isToday(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
});
const older: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return !isThisWeek(new Date(pageUpdatedAt)) && !isYesterday(new Date(pageUpdatedAt));
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && !isThisWeek(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
});
return { today, yesterday, this_week, older };