Compare commits

..
Author SHA1 Message Date
sriram veeraghantaandGitHub c1b1ba35c1 Merge pull request #3878 from makeplane/preview
release: v0.16-dev
2024-03-05 20:04:08 +05:30
sriram veeraghantaandGitHub 4566d6e80c Merge pull request #3697 from makeplane/preview
release: 0.15.4-dev
2024-02-19 19:30:06 +05:30
sriram veeraghantaandGitHub e8d359e625 Merge pull request #3674 from makeplane/preview
fix: build branch docker images push on release
2024-02-15 14:35:32 +05:30
sriram veeraghantaandGitHub 351eba8d61 Merge pull request #3671 from makeplane/preview
release: peek overview issue description initial load bug (#3670)
2024-02-15 03:25:30 +05:30
sriram veeraghantaandGitHub 1e27e37b51 Merge pull request #3666 from makeplane/preview
release: v0.15.2-dev
2024-02-14 19:41:55 +05:30
sriram veeraghantaandGitHub 7df2e9cf11 Merge pull request #3632 from makeplane/preview
release: v0.15.1-dev
2024-02-12 20:59:56 +05:30
sriram veeraghantaandGitHub c6e3f1b932 Merge pull request #3535 from makeplane/preview
release: 0.15-dev
2024-02-01 15:01:49 +05:30
65 changed files with 479 additions and 1105 deletions
-97
View File
@@ -1,97 +0,0 @@
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"
+6 -5
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 == 'workflow_dispatch' || 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 == '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 == 'workflow_dispatch' || 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 == '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 == 'workflow_dispatch' || 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 == '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 == 'workflow_dispatch' || 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 == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -280,3 +280,4 @@ jobs:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+27 -84
View File
@@ -1,19 +1,27 @@
name: Build and Lint on Pull Request
name: Build Pull Request Contents
on:
workflow_dispatch:
pull_request:
types: ["opened", "synchronize"]
jobs:
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 }}
build-pull-request-contents:
name: Build Pull Request Contents
runs-on: ubuntu-20.04
permissions:
pull-requests: read
steps:
- uses: actions/checkout@v3
- 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
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
@@ -23,82 +31,17 @@ jobs:
- apiserver/**
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
deploy:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
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 Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
yarn
yarn build --filter=web
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
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
yarn
yarn build --filter=space
-45
View File
@@ -1,45 +0,0 @@
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 }}
+33 -31
View File
@@ -2,11 +2,12 @@ name: "CodeQL"
on:
push:
branches: ["master"]
branches: [ 'develop', 'preview', 'master' ]
pull_request:
branches: ["develop", "preview", "master"]
# The branches below must be a subset of the branches above
branches: [ 'develop', 'preview', 'master' ]
schedule:
- cron: "53 19 * * 5"
- cron: '53 19 * * 5'
jobs:
analyze:
@@ -20,44 +21,45 @@ 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
# ️ 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
# 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
# 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.
# ️ 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
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# 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.
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
# - 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}}"
-199
View File
@@ -1,199 +0,0 @@
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
+14 -24
View File
@@ -88,10 +88,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
Q(project_projectmember__member=self.request.user)
| Q(network=2)
)
.select_related(
@@ -176,7 +173,10 @@ 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,11 +576,9 @@ 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
@@ -687,14 +685,9 @@ 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)
@@ -717,9 +710,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(
@@ -740,10 +733,7 @@ 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)
+44 -45
View File
@@ -1,79 +1,78 @@
# One-click deploy
# 1-Click Self-Hosting
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
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.
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.
Let's get started!
### Requirements
## Installing Plane
- Operating systems: Debian, Ubuntu, CentOS
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
Installing Plane is a very easy and minimal step process.
### Download the latest stable release
### Prerequisite
Run ↓ on any CLI.
- Operating System (latest): Debian / Ubuntu / Centos
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
### Downloading Latest Stable Release
```
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
```
### Download the Preview release
`Preview` builds do not support ARM64, AArch64 CPU architectures
Run ↓ on any CLI.
<details>
<summary>Downloading Preview Release</summary>
```
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>
### Successful installation
--
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
Expect this after a successful install
![Install Output](images/install.png)
Access the application on a browser via http://server-ip-address
---
### Manage your Plane instance
### Get Control of your Plane Server Setup
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
![Plane Help](images/help.png)
1. Basic operators
<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. `plane-app start` starts the Plane server.
2. `plane-app restart` restarts the Plane server.
3. `plane-app stop` stops the Plane server.
<ins>Advanced Operations</ins>:
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)
2. Advanced operators
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
`plane-app --configure` will show advanced configurators.
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
- 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. 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.
3. Version operators
1. Plane App can be reinstalled using `plane-app --install`.
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.
<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
+12
View File
@@ -494,6 +494,13 @@ 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 ""
@@ -599,6 +606,11 @@ 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,7 +7,6 @@ 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;
@@ -73,7 +72,7 @@ export const EditorHeader = (props: IEditorHeader) => {
Icon={Archive}
backgroundColor="bg-blue-500/20"
textColor="text-blue-500"
label={`Archived at ${getDate(archivedAt)?.toLocaleString()}`}
label={`Archived at ${new Date(archivedAt).toLocaleString()}`}
/>
)}
@@ -3,15 +3,13 @@ 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 | undefined): string => {
const renderDate = (date: Date): string => {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "long",
@@ -54,14 +52,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(getDate(documentDetails?.last_updated_at))}
{renderDate(new Date(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(getDate(documentDetails?.created_on))}
{renderDate(new Date(documentDetails.created_on))}
</h5>
</div>
</div>
@@ -1,22 +0,0 @@
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: string | null;
created_at: Date;
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: string | null;
updated_at: Date;
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, getDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
type Props = {
title: string;
@@ -43,10 +43,7 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
handleClose();
};
const date1 = getDate(watch("date1"));
const date2 = getDate(watch("date1"));
const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false;
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
return (
<Transition.Root show={isOpen} as={Fragment}>
@@ -89,45 +86,35 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
<Controller
control={control}
name="date1"
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"
/>
);
}}
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"
/>
)}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
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"
/>
);
}}
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"
/>
)}
/>
)}
</div>
@@ -7,8 +7,6 @@ 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";
@@ -252,17 +250,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
/>
)}
/>
<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 gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
{responseActionButton}
<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 { getDate, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import { 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 = getDate(startDate);
const end = getDate(endDate);
const start = new Date(startDate);
const end = new Date(endDate);
let dates: Date[] = [];
if (start && end && isValid(start) && isValid(end)) {
if (isValid(start) && isValid(end)) {
dates = eachDayOfInterval({ start, end });
}
+8 -11
View File
@@ -27,12 +27,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers
import {
renderFormattedDate,
findHowManyDaysLeft,
renderFormattedDateWithoutYear,
getDate,
} from "helpers/date-time.helper";
import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import { truncateText } from "helpers/string.helper";
// types
import { ICycle, TCycleGroups } from "@plane/types";
@@ -107,10 +102,8 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
/>
);
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 endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? "");
const groupedIssues: any = {
backlog: activeCycle.backlog_issues,
@@ -120,6 +113,8 @@ 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;
@@ -156,6 +151,8 @@ 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">
@@ -314,7 +311,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{currentProjectDetails?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
<Tooltip position="top-left" tooltipContent={issue.name}>
<Tooltip position="top-left" tooltipHeading="Title" 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, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, 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 = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(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, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, 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 = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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: getDate(startDateValue),
to: getDate(endDateValue),
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -8,7 +8,6 @@ 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";
@@ -46,8 +45,8 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
data: block,
id: block?.id ?? "",
sort_order: block?.sort_order ?? 0,
start_date: getDate(block?.start_date),
target_date: getDate(block?.end_date),
start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block?.end_date ?? ""),
}));
return structuredBlocks;
+4 -4
View File
@@ -96,10 +96,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
};
const dateChecker = async (projectId: string, payload: CycleDateCheckData) => {
const dateChecker = async (payload: CycleDateCheckData) => {
let status = false;
await cycleService.cycleDateCheck(workspaceSlug, projectId, payload).then((res) => {
await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, 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(payload.project_id ?? projectId, {
isDateValid = await dateChecker({
start_date: payload.start_date,
end_date: payload.end_date,
cycle_id: data.id,
});
else
isDateValid = await dateChecker(payload.project_id ?? projectId, {
isDateValid = await dateChecker({
start_date: payload.start_date,
end_date: payload.end_date,
});
+5 -8
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,11 +186,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
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 isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const progressPercentage = cycleDetails
? isCompleted && cycleDetails?.progress_snapshot
@@ -320,8 +317,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: getDate(startDateValue),
to: getDate(endDateValue),
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
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, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue, TWidgetIssue } from "@plane/types";
@@ -34,8 +34,6 @@ 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}`}
@@ -50,7 +48,11 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
<h6 className="text-sm flex-grow truncate">{issueDetails.name}</h6>
</div>
<div className="text-xs text-center">
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
{issueDetails.target_date
? isToday(new Date(issueDetails.target_date))
? "Today"
: renderFormattedDate(issueDetails.target_date)
: "-"}
</div>
<div className="text-xs text-center">
{blockedByIssues.length > 0
@@ -81,7 +83,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false) ?? 0;
return (
<ControlLink
@@ -210,7 +212,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
const projectDetails = getProjectById(issue.project_id);
const dueBy = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(new Date(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 { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { 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={getDate(value)}
defaultMonth={getDate(value)}
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => {
dropdownOnChange(date ?? null);
}}
+2 -3
View File
@@ -2,7 +2,7 @@ import { useState, FC } from "react";
// ui
import { Button } from "@plane/ui";
// helpers
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { IExportData } from "@plane/types";
@@ -18,8 +18,7 @@ export const SingleExport: FC<Props> = ({ service, refreshing }) => {
const checkExpiry = (inputDateString: string) => {
const currentDate = new Date();
const expiryDate = getDate(inputDateString);
if (!expiryDate) return false;
const expiryDate = new Date(inputDateString);
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: ChartDataType, blocks: IGanttBlock[] | null) =>
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: IGanttBlock) => ({
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
+2 -2
View File
@@ -6,8 +6,8 @@ export interface IGanttBlock {
width: number;
};
sort_order: number;
start_date: Date | undefined;
target_date: Date | undefined;
start_date: Date | null;
target_date: Date | null;
}
export interface IBlockUpdateData {
@@ -1,5 +1,4 @@
// types
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { ChartDataType, IGanttBlock } from "../types";
// data
import { weeks, months } from "../data";
@@ -168,16 +167,15 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType,
const { startDate } = chartData.data;
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
if (!itemStartDate || !itemTargetDate) return;
if (!itemStartDate || !itemTargetDate) return null;
startDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0);
itemTargetDate.setHours(0, 0, 0, 0);
const positionDaysDifference = findTotalDaysInRange(startDate, itemStartDate, false);
if (!positionDaysDifference) return;
// position code starts
const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime();
const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)));
scrollPosition = positionDaysDifference * chartData.data.width;
var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12;
+11 -17
View File
@@ -21,8 +21,6 @@ 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;
@@ -173,11 +171,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const today = new Date();
const tomorrow = getDate(today);
tomorrow?.setDate(today.getDate() + 1);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
useEffect(() => {
if (!issueStatus || !issueStatus.snoozed_till) return;
setDate(issueStatus.snoozed_till);
setDate(new Date(issueStatus.snoozed_till));
}, [issueStatus]);
if (!issueStatus || !issue || !inboxIssues) return <></>;
@@ -271,23 +269,19 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{({ close }) => (
<div className="flex h-full w-full flex-col gap-y-1">
<DayPicker
selected={getDate(date)}
defaultMonth={getDate(date)}
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => {
if (!date) return;
setDate(date);
}}
mode="single"
className="border border-custom-border-200 rounded-md p-3"
disabled={
tomorrow
? [
{
before: tomorrow,
},
]
: undefined
}
disabled={[
{
before: tomorrow,
},
]}
/>
<Button
variant="primary"
@@ -295,7 +289,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
close();
inboxIssueOperations.updateInboxIssueStatus({
status: 0,
snoozed_till: date,
snoozed_till: new Date(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 && !!inboxIssueDetail.snoozed_till && inboxIssueDetail.snoozed_till < new Date();
inboxIssueDetail.status === 0 && new Date(inboxIssueDetail.snoozed_till ?? "") < new Date();
return (
<div
@@ -46,7 +46,7 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
workspaceSlug,
projectId,
inboxIssueDetail.duplicate_to ?? "",
inboxIssueDetail.snoozed_till
new Date(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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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 ? getDate(issue.start_date) : null;
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
@@ -28,13 +28,12 @@ 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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { cn } from "helpers/common.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
@@ -100,10 +99,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 ? getDate(issue.start_date) : null;
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? getDate(issue.target_date) : null;
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
return (
@@ -12,8 +12,6 @@ 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;
@@ -49,10 +47,8 @@ export const CalendarMonthsDropdown: React.FC<Props> = observer((props: Props) =
const daysList = Object.keys(allDaysOfActiveWeek);
const firstDay = getDate(daysList[0]);
const lastDay = getDate(daysList[daysList.length - 1]);
if (!firstDay || !lastDay) return "Week view";
const firstDay = new Date(daysList[0]);
const lastDay = new Date(daysList[daysList.length - 1]);
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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" 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 tooltipContent={issueDetails?.name}>
<Tooltip tooltipHeading="Title" 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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" 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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
@@ -58,7 +58,6 @@ export interface IGroupByKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@@ -84,7 +83,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props;
const member = useMember();
@@ -99,27 +97,17 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null;
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0);
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) => {
if (sub_group_by) {
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
return false;
} else {
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
else groupVisibility.showGroup = false;
}
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
return groupVisibility;
if (kanbanFilters?.group_by.includes(_list.id)) return true;
return false;
}
};
@@ -127,18 +115,13 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
return (
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
{groupList &&
groupList.length > 0 &&
groupList.map((_list: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list);
if (groupByVisibilityToggle.showGroup === false) return <></>;
return (
<div
className={`relative flex flex-shrink-0 flex-col group ${
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
} `}
>
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `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
@@ -158,7 +141,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div>
)}
{groupByVisibilityToggle.showIssues && (
{!groupByVisibilityToggle && (
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
@@ -176,6 +159,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
@@ -213,7 +197,6 @@ 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) => {
@@ -238,7 +221,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup,
subGroupIssueHeaderCount,
} = props;
const issueKanBanView = useKanbanView();
@@ -266,7 +248,6 @@ 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,7 +29,6 @@ interface ISubGroupSwimlaneHeader {
list: IGroupByColumn[];
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
}
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@@ -40,22 +39,6 @@ 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,
@@ -63,36 +46,25 @@ 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) => {
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>
);
})}
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>
))}
</div>
);
@@ -152,74 +124,52 @@ 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) => {
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
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" />
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>
{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 className="w-full border-b border-dashed border-custom-border-400" />
</div>
);
})}
{!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}
/>
</div>
)}
</div>
))}
</div>
);
});
@@ -311,7 +261,6 @@ 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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" 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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
@@ -19,7 +19,7 @@ import {
StateDropdown,
} from "components/dropdowns";
// helpers
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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 = getDate(issue.start_date);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = getDate(issue.target_date);
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
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 truncate">
<div className="h-5">
<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}
maxDate={maxDate ?? undefined}
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}
minDate={minDate ?? undefined}
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 { cn } from "helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { cn } from "helpers/common.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={getDate(issue.start_date)}
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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={getDate(issue.target_date)}
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
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 tooltipContent={issueDetail.name}>
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 text-left text-[0.825rem] text-custom-text-100 focus:outline-none"
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"
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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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 = getDate(startDate);
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = getDate(targetDate);
const maxDate = targetDate ? new Date(targetDate) : null;
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 = getDate(issue.start_date);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = getDate(issue.target_date);
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
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 tooltipContent={issue.name}>
<Tooltip tooltipHeading="Title" 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 { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { 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: getDate(startDateValue),
to: getDate(endDateValue),
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -7,7 +7,6 @@ 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
@@ -33,8 +32,8 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: getDate(block.start_date),
target_date: getDate(block.target_date),
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
};
});
+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 { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { 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 = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(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 { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { 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 = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(moduleDetails.start_date ?? "");
const renderDate = moduleDetails.start_date || moduleDetails.target_date;
+23 -29
View File
@@ -25,7 +25,7 @@ import { DateRangeDropdown, MemberDropdown } from "components/dropdowns";
// ui
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
@@ -201,10 +201,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
});
}, [moduleDetails, reset]);
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 isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`);
const progressPercentage = moduleDetails
? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
@@ -344,30 +342,26 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Controller
control={control}
name="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",
}}
/>
);
}}
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",
}}
/>
)}
/>
)}
/>
@@ -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, getDate } from "helpers/date-time.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
// type
import type { IUserNotification, NotificationType } from "@plane/types";
// constants
@@ -119,9 +119,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const notificationField = notification.data.issue_activity.field;
const notificationTriggeredBy = notification.triggered_by_details;
const snoozedTillDate = getDate(notification?.snoozed_till);
if (snoozedTillDate && isSnoozedTabOpen && snoozedTillDate < new Date()) return null;
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
return (
<Link
@@ -12,8 +12,6 @@ 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;
@@ -62,7 +60,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
if (!formDataDate) return timeStamps;
const isToday = today.toDateString() === getDate(formDataDate)?.toDateString();
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
if (!isToday) return timeStamps;
@@ -95,9 +93,9 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
);
const minutes = parseInt(time[1]);
const dateTime: Date | undefined = getDate(formData?.date);
dateTime?.setHours(hours);
dateTime?.setMinutes(minutes);
const dateTime = new Date(formData.date);
dateTime.setHours(hours);
dateTime.setMinutes(minutes);
await handleSubmitSnooze(notification.id, dateTime).then(() => {
handleClose();
@@ -216,11 +214,10 @@ 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>
@@ -228,11 +225,10 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "PM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "PM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
PM
</div>
+1 -6
View File
@@ -8,12 +8,7 @@ export const INBOX_STATUS: {
status: number;
icon: LucideIcon;
title: string;
description: (
workspaceSlug: string,
projectId: string,
issueId: string,
snoozedTillDate: Date | undefined
) => JSX.Element;
description: (workspaceSlug: string, projectId: string, issueId: string, snoozedTillDate: Date) => 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 } = state;
const { selectedTab, snoozed, archived, readNotification, selectedNotificationForSnooze } = 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 : dateTime }
? { ...notification, snoozed_till: isSnoozed ? null : new Date(dateTime!) }
: notification
) || [],
false
+19 -48
View File
@@ -1,5 +1,5 @@
import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns";
import isNumber from "lodash/isNumber";
import { isNil } from "lodash";
// Format Date Helpers
/**
@@ -8,11 +8,10 @@ import isNumber from "lodash/isNumber";
* @param {Date | string} date
* @example renderFormattedDate("2024-01-01") // Jan 01, 2024
*/
export const renderFormattedDate = (date: string | Date | undefined | null): string | null => {
export const renderFormattedDate = (date: string | Date): string | null => {
if (!date) return null;
// Parse the date to check if it is valid
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return null;
const parsedDate = new Date(date);
// 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)
@@ -27,10 +26,9 @@ export const renderFormattedDate = (date: string | Date | undefined | null): str
* @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 = getDate(date);
// return if undefined
if (!parsedDate) return "";
const parsedDate = new Date(date);
// 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)
@@ -45,10 +43,9 @@ 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 = getDate(date);
// return if undefined
if (!parsedDate) return null;
const parsedDate = new Date(date);
// 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)
@@ -65,14 +62,10 @@ 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 | undefined | null,
timeFormat: "12-hour" | "24-hour" = "24-hour"
): string => {
export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => {
if (!date || date === "") return "";
// Parse the date to check if it is valid
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return "";
const parsedDate = new Date(date);
// 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
@@ -99,11 +92,10 @@ 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 = getDate(startDate);
const parsedEndDate = getDate(endDate);
// return if undefined
if (!parsedStartDate || !parsedEndDate) return;
const parsedStartDate = new Date(startDate);
const parsedEndDate = new Date(endDate);
// 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
@@ -139,7 +131,6 @@ 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 });
@@ -173,7 +164,7 @@ export const isDateGreaterThanToday = (dateStr: string): boolean => {
* @example getWeekNumber(new Date("2023-09-01")) // 35
*/
export const getWeekNumberOfDate = (date: Date): number => {
const currentDate = date;
const currentDate = new Date(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
@@ -195,30 +186,10 @@ export const checkIfDatesAreEqual = (
date1: Date | string | null | undefined,
date2: Date | string | null | undefined
): boolean => {
const parsedDate1 = getDate(date1);
const parsedDate2 = getDate(date2);
// return if undefined
if (!parsedDate1 && !parsedDate2) return true;
if (!parsedDate1 || !parsedDate2) return false;
if (isNil(date1) && isNil(date2)) return true;
if (isNil(date1) || isNil(date2)) 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,7 +1,5 @@
// 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
@@ -16,35 +14,3 @@ 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;
};
+3 -6
View File
@@ -1,7 +1,6 @@
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 {
@@ -158,9 +157,7 @@ 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 = getDate(date);
if (!parsedDate) return false;
const parsedDate = new Date(date);
const targetDateDistance = differenceInCalendarDays(parsedDate, new Date());
// if the issue is overdue, highlight the due date
@@ -171,6 +168,6 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: getDate(block.start_date),
target_date: getDate(block.target_date),
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
}));
@@ -21,7 +21,6 @@ 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";
@@ -272,8 +271,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
@@ -283,7 +282,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: getDate(archived_at),
archived_at: archived_at ? new Date(archived_at) : undefined,
}
: undefined
}
@@ -299,8 +298,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
+4 -11
View File
@@ -4,10 +4,6 @@ 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";
@@ -122,9 +118,8 @@ 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 endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
const isEndDateToday = endDate && isToday(endDate);
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
const isEndDateToday = isToday(new Date(c.end_date ?? ""));
return c.project_id === projectId && hasEndDatePassed && !isEndDateToday;
});
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
@@ -139,8 +134,7 @@ 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 startDate = getDate(c.start_date);
const isStartDateUpcoming = startDate && isFuture(startDate);
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
return c.project_id === projectId && isStartDateUpcoming;
});
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
@@ -155,8 +149,7 @@ 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 endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
return c.project_id === projectId && !hasEndDatePassed;
});
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
+35 -38
View File
@@ -1,17 +1,16 @@
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";
@@ -63,35 +62,35 @@ export class IssueHelperStore implements TIssueHelperStore {
issues: TIssueMap,
isCalendarIssues: boolean = false
) => {
const currentIssues: { [group_id: string]: string[] } = {};
if (!groupBy) return currentIssues;
const _issues: { [group_id: string]: string[] } = {};
if (!groupBy) return _issues;
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
currentIssues[group] = [];
_issues[group] = [];
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const currentIssue = projectIssues[issue];
const _issue = projectIssues[issue];
let groupArray = [];
if (groupBy === "state_detail.group") {
// 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";
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
groupArray = [state_group];
} else {
const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"];
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : [];
}
for (const group of groupArray) {
if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id);
else if (group) currentIssues[group] = [currentIssue.id];
if (group && _issues[group]) _issues[group].push(_issue.id);
else if (group) _issues[group] = [_issue.id];
}
}
return currentIssues;
return _issues;
};
subGroupedIssues = (
@@ -100,47 +99,45 @@ export class IssueHelperStore implements TIssueHelperStore {
orderBy: TIssueOrderByOptions,
issues: TIssueMap
) => {
const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return currentIssues;
const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return _issues;
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => {
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => {
const groupByIssues: { [group_id: string]: string[] } = {};
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
groupByIssues[group] = [];
});
currentIssues[sub_group] = groupByIssues;
_issues[sub_group] = groupByIssues;
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const currentIssue = projectIssues[issue];
const _issue = projectIssues[issue];
let subGroupArray = [];
let groupArray = [];
if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") {
const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None";
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
subGroupArray = [state_group];
groupArray = [state_group];
} else {
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"];
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) : [];
}
for (const subGroup of subGroupArray) {
for (const group of groupArray) {
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] };
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] };
}
}
}
return currentIssues;
return _issues;
};
unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) =>
@@ -218,8 +215,8 @@ export class IssueHelperStore implements TIssueHelperStore {
const moduleMap = this.rootStore?.moduleMap;
if (!moduleMap) break;
for (const dataId of dataIdsArray) {
const currentModule = moduleMap[dataId];
if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase());
const _module = moduleMap[dataId];
if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase());
}
break;
case "cycle_id":
@@ -236,10 +233,10 @@ export class IssueHelperStore implements TIssueHelperStore {
}
/**
* This Method is mainly used to filter out empty values in the beginning
* This Method is mainly used to filter out empty values in the begining
* @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 empty, 0 if not empty
* @returns 1 if emoty, 0 if not empty
*/
getSortOrderToFilterEmptyValues(key: string, object: any) {
const value = object?.[key];
@@ -391,7 +388,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 && value.length) return value;
if (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: string | null;
created_at: Date;
created_by: string;
description: string;
description_html: string;
@@ -23,7 +23,7 @@ export interface IPageStore {
name: string;
owned_by: string;
project: string;
updated_at: string | null;
updated_at: Date;
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: string | null;
created_at: Date;
created_by: string;
description: string;
description_html = "";
@@ -64,7 +64,7 @@ export class PageStore implements IPageStore {
name = "";
owned_by: string;
project: string;
updated_at: string | null;
updated_at: Date;
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 ?? "";
this.created_at = page?.created_at || new Date();
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 ?? "";
this.updated_at = page?.updated_at || new Date();
this.project = page?.project || "";
this.owned_by = page?.owned_by || "";
this.labels = page?.labels || [];
+18 -17
View File
@@ -8,7 +8,6 @@ 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;
@@ -74,8 +73,8 @@ export class ProjectPageStore implements IProjectPageStore {
const allProjectIds = Object.keys(this.projectPageMap[projectId]);
return allProjectIds.sort((a, b) => {
const dateA = getDate(this.projectPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
const dateA = new Date(this.projectPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectPageMap[projectId][b].created_at).getTime();
return dateB - dateA;
});
}
@@ -85,8 +84,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 = getDate(this.projectArchivedPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectArchivedPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
const dateA = new Date(this.projectArchivedPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectArchivedPageMap[projectId][b].created_at).getTime();
return dateB - dateA;
});
}
@@ -127,24 +126,26 @@ export class ProjectPageStore implements IProjectPageStore {
const projectId = this.rootStore.app.router.projectId;
if (!this.projectPageIds || !projectId) return;
const today: string[] = this.projectPageIds.filter((page) => {
const updatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return updatedAt && isToday(updatedAt);
});
const today: string[] = this.projectPageIds.filter((page) =>
isToday(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 yesterday: string[] = this.projectPageIds.filter((page) =>
isYesterday(new Date(this.projectPageMap[projectId][page].updated_at))
);
const this_week: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && isThisWeek(pageUpdatedAt) && !isToday(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return (
isThisWeek(new Date(pageUpdatedAt)) &&
!isToday(new Date(pageUpdatedAt)) &&
!isYesterday(new Date(pageUpdatedAt))
);
});
const older: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && !isThisWeek(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return !isThisWeek(new Date(pageUpdatedAt)) && !isYesterday(new Date(pageUpdatedAt));
});
return { today, yesterday, this_week, older };