Compare commits

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

* fix failing build

* fix gantt chart view position fix

* comments for getDate method

* remove new Date() where not required

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

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

* fixes
2024-03-08 15:16:32 +05:30
56 changed files with 831 additions and 477 deletions
+5 -6
View File
@@ -18,7 +18,7 @@ jobs:
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
@@ -74,7 +74,7 @@ jobs:
- nginx/**
branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -126,7 +126,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -178,7 +178,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -230,7 +230,7 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -280,4 +280,3 @@ jobs:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+31 -33
View File
@@ -2,12 +2,11 @@ name: "CodeQL"
on:
push:
branches: [ 'develop', 'preview', 'master' ]
branches: ["master"]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'develop', 'preview', 'master' ]
branches: ["develop", "preview", "master"]
schedule:
- cron: '53 19 * * 5'
- cron: "53 19 * * 5"
jobs:
analyze:
@@ -21,45 +20,44 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'python', 'javascript' ]
language: ["python", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+157 -31
View File
@@ -4,70 +4,196 @@ on:
workflow_dispatch:
inputs:
web-build:
required: true
required: false
description: 'Build Web'
type: boolean
default: true
space-build:
required: true
required: false
description: 'Build Space'
type: boolean
default: false
env:
BUILD_WEB: ${{ github.event.inputs.web-build }}
BUILD_SPACE: ${{ github.event.inputs.space-build }}
jobs:
setup-feature-build:
name: Feature Build Setup
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
echo "BUILD_WEB=$BUILD_WEB"
echo "BUILD_SPACE=$BUILD_SPACE"
outputs:
web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
feature-build-web:
if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }}
needs: setup-feature-build
name: Feature Build Web
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Web
id: build-web
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=web
cd $GITHUB_WORKSPACE
TAR_NAME="web.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
name: Feature Build Space
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }}
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Checkout
uses: actions/checkout@v4
with:
path: plane
- name: Install Dependencies
run: |
cd $GITHUB_WORKSPACE/plane
yarn install
- name: Build Space
id: build-space
run: |
cd $GITHUB_WORKSPACE/plane
yarn build --filter=space
cd $GITHUB_WORKSPACE
TAR_NAME="space.tar.gz"
tar -czf $TAR_NAME ./plane
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }}
needs: [feature-build-web, feature-build-space]
name: Feature Deploy
runs-on: ubuntu-latest
env:
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }}
BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }}
BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }}
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }}
steps:
- name: Install AWS cli
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install awscli
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
tags: tag:ci
- name: Kubectl Setup
run: |
curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl"
chmod +x kubectl
mkdir -p ~/.kube
echo "$KUBE_CONFIG_FILE" > ~/.kube/config
chmod 600 ~/.kube/config
- name: HELM Setup
run: |
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
- name: App Deploy
run: |
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }}
GIT_BRANCH=${{ github.ref_name }}
APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }}
WEB_S3_URL=""
if [ ${{ env.BUILD_WEB }} == true ]; then
WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600)
fi
METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--kube-insecure-skip-tls-verify \
--generate-name \
--namespace $APP_NAMESPACE \
--set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \
--set shared_config.git_branch="$GIT_BRANCH" \
--set web.enabled=${{ env.BUILD_WEB }} \
--set space.enabled=${{ env.BUILD_SPACE }} \
--output json \
--timeout 1000s)
SPACE_S3_URL=""
if [ ${{ env.BUILD_SPACE }} == true ]; then
SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600)
fi
APP_NAME=$(echo $METADATA | jq -r '.name')
if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then
INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }}
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}"
DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}"
METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \
--generate-name \
--namespace $APP_NAMESPACE \
--set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \
--set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set web.enabled=${{ env.BUILD_WEB || false }} \
--set web.artifact_url=$WEB_S3_URL \
--set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \
--set space.enabled=${{ env.BUILD_SPACE || false }} \
--set space.artifact_url=$SPACE_S3_URL \
--set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \
--set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \
--output json \
--timeout 1000s)
APP_NAME=$(echo $METADATA | jq -r '.name')
INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \
-o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \
jq -r '.spec.rules[0].host')
echo "****************************************"
echo "APP NAME ::: $APP_NAME"
echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME"
echo "****************************************"
fi
+24 -14
View File
@@ -88,7 +88,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(
Q(project_projectmember__member=self.request.user)
Q(
project_projectmember__member=self.request.user,
project_projectmember__is_active=True,
)
| Q(network=2)
)
.select_related(
@@ -173,10 +176,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
for field in request.GET.get("fields", "").split(",")
if field
]
projects = (
self.get_queryset()
.order_by("sort_order", "name")
)
projects = self.get_queryset().order_by("sort_order", "name")
if request.GET.get("per_page", False) and request.GET.get(
"cursor", False
):
@@ -576,9 +576,11 @@ class ProjectJoinEndpoint(BaseAPIView):
_ = WorkspaceMember.objects.create(
workspace_id=project_invite.workspace_id,
member=user,
role=15
if project_invite.role >= 15
else project_invite.role,
role=(
15
if project_invite.role >= 15
else project_invite.role
),
)
else:
# Else make him active
@@ -685,9 +687,14 @@ class ProjectMemberViewSet(BaseViewSet):
)
bulk_project_members = []
member_roles = {member.get("member_id"): member.get("role") for member in members}
member_roles = {
member.get("member_id"): member.get("role") for member in members
}
# Update roles in the members array based on the member_roles dictionary
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
for project_member in ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
):
project_member.role = member_roles[str(project_member.member_id)]
project_member.is_active = True
bulk_project_members.append(project_member)
@@ -710,9 +717,9 @@ class ProjectMemberViewSet(BaseViewSet):
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000
if len(sort_order)
else 65535,
sort_order=(
sort_order[0] - 10000 if len(sort_order) else 65535
),
)
)
bulk_issue_props.append(
@@ -733,7 +740,10 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
project_members = ProjectMember.objects.filter(
project_id=project_id,
member_id__in=[member.get("member_id") for member in members],
)
serializer = ProjectMemberRoleSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
+50 -49
View File
@@ -1,78 +1,79 @@
# 1-Click Self-Hosting
# One-click deploy
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
Let's get started!
This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
## Installing Plane
### Requirements
Installing Plane is a very easy and minimal step process.
- Operating systems: Debian, Ubuntu, CentOS
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
### Prerequisite
### Download the latest stable release
- Operating System (latest): Debian / Ubuntu / Centos
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
### Downloading Latest Stable Release
Run ↓ on any CLI.
```
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
```
<details>
<summary>Downloading Preview Release</summary>
### Download the Preview release
`Preview` builds do not support ARM64, AArch64 CPU architectures
Run ↓ on any CLI.
```
export BRANCH=preview
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
```
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
</details>
--
Expect this after a successful install
![Install Output](images/install.png)
Access the application on a browser via http://server-ip-address
---
### Get Control of your Plane Server Setup
### Successful installation
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane
You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
![Install Output](images/install.png)
---
### Manage your Plane instance
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
![Plane Help](images/help.png)
<ins>Basic Operations</ins>:
1. Start Server using `plane-app start`
1. Stop Server using `plane-app stop`
1. Restart Server using `plane-app restart`
1. Basic operators
<ins>Advanced Operations</ins>:
1. Configure Plane using `plane-app --configure`. This will give you options to modify
- NGINX Port (default 80)
- Domain Name (default is the local server public IP address)
- File Upload Size (default 5MB)
- External Postgres DB Url (optional - default empty)
- External Redis URL (optional - default empty)
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket)
1. `plane-app start` starts the Plane server.
2. `plane-app restart` restarts the Plane server.
3. `plane-app stop` stops the Plane server.
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images)
2. Advanced operators
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility.
`plane-app --configure` will show advanced configurators.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio.
- Change your proxy or listening port
<br>Default: 80
- Change your domain name
<br>Default: Deployed server's public IP address
- File upload size
<br>Default: 5MB
- Specify external database address when using an external database
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/postgres`
- Specify external Redis URL when using external Redis
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/redis`
- Configure AWS S3 bucket
<br>Use only when you or your users want to use S3
<br>`Default folder: /opt/plane/data/minio`
1. Plane App can be reinstalled using `plane-app --install`.
3. Version operators
<ins>Application Data is stored in the mentioned folders</ins>:
1. DB Data: /opt/plane/data/postgres
1. Redis Data: /opt/plane/data/redis
1. Minio Data: /opt/plane/data/minio
1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
Postgres, Redis, and Minio alone.
4. `plane-app --install` installs the Plane app again.
-12
View File
@@ -494,13 +494,6 @@ function install() {
update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP"
update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')"
if command -v crontab &> /dev/null; then
sudo touch /etc/cron.daily/makeplane
sudo chmod +x /etc/cron.daily/makeplane
sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane
sudo crontab /etc/cron.daily/makeplane
fi
show_message "Plane Installed Successfully ✅"
show_message ""
@@ -606,11 +599,6 @@ function uninstall() {
sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
if command -v crontab &> /dev/null; then
sudo crontab -r &> /dev/null
sudo rm /etc/cron.daily/makeplane &> /dev/null
fi
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
show_message "- Configuration Cleaned ✅"
@@ -7,6 +7,7 @@ import { AlertLabel } from "src/ui/components/alert-label";
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "src/ui/components/vertical-dropdown-menu";
import { SummaryPopover } from "src/ui/components/summary-popover";
import { InfoPopover } from "src/ui/components/info-popover";
import { getDate } from "src/utils/date-utils";
interface IEditorHeader {
editor: Editor;
@@ -72,7 +73,7 @@ export const EditorHeader = (props: IEditorHeader) => {
Icon={Archive}
backgroundColor="bg-blue-500/20"
textColor="text-blue-500"
label={`Archived at ${new Date(archivedAt).toLocaleString()}`}
label={`Archived at ${getDate(archivedAt)?.toLocaleString()}`}
/>
)}
@@ -3,13 +3,15 @@ import { usePopper } from "react-popper";
import { Calendar, History, Info } from "lucide-react";
// types
import { DocumentDetails } from "src/types/editor-types";
//utils
import { getDate } from "src/utils/date-utils";
type Props = {
documentDetails: DocumentDetails;
};
// function to render a Date in the format- 25 May 2023 at 2:53PM
const renderDate = (date: Date): string => {
const renderDate = (date: Date | undefined): string => {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "long",
@@ -52,14 +54,14 @@ export const InfoPopover: React.FC<Props> = (props) => {
<h6 className="text-xs text-custom-text-400">Last updated on</h6>
<h5 className="flex items-center gap-1 text-sm">
<History className="h-3 w-3" />
{renderDate(new Date(documentDetails.last_updated_at))}
{renderDate(getDate(documentDetails?.last_updated_at))}
</h5>
</div>
<div className="space-y-1.5">
<h6 className="text-xs text-custom-text-400">Created on</h6>
<h5 className="flex items-center gap-1 text-sm">
<Calendar className="h-3 w-3" />
{renderDate(new Date(documentDetails.created_on))}
{renderDate(getDate(documentDetails?.created_on))}
</h5>
</div>
</div>
@@ -0,0 +1,22 @@
function isNumber(value: any) {
return typeof value === "number";
}
/**
* This method returns a date from string of type yyyy-mm-dd
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
* @param date
* @returns date or undefined
*/
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
if (!date || date === "") return;
if (typeof date !== "string" && !(date instanceof String)) return date;
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
const year = parseInt(yearString);
const month = parseInt(monthString);
const day = parseInt(dayString);
if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return;
return new Date(year, month - 1, day);
};
+2 -2
View File
@@ -6,7 +6,7 @@ export interface IPage {
archived_at: string | null;
blocks: IPageBlock[];
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html: string;
@@ -20,7 +20,7 @@ export interface IPage {
owned_by: string;
project: string;
project_detail: IProjectLite;
updated_at: Date;
updated_at: string | null;
updated_by: string;
workspace: string;
workspace_detail: IWorkspaceLite;
@@ -8,7 +8,7 @@ import { DateFilterSelect } from "./date-filter-select";
// ui
import { Button } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper";
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "helpers/date-time.helper";
type Props = {
title: string;
@@ -43,7 +43,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
handleClose();
};
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
const date1 = getDate(watch("date1"));
const date2 = getDate(watch("date1"));
const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false;
return (
<Transition.Root show={isOpen} as={Fragment}>
@@ -86,35 +89,45 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
<Controller
control={control}
name="date1"
render={({ field: { value, onChange } }) => (
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ after: new Date(watch("date2")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date2Value = getDate(watch("date2"));
return (
<DayPicker
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date2Value ? [{ after: date2Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/>
);
}}
/>
{watch("filterType") === "range" && (
<Controller
control={control}
name="date2"
render={({ field: { value, onChange } }) => (
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
onSelect={(date) => onChange(date)}
mode="single"
disabled={[
{ before: new Date(watch("date1")) }
]}
className="border border-custom-border-200 p-3 rounded-md"
/>
)}
render={({ field: { value, onChange } }) => {
const dateValue = getDate(value);
const date1Value = getDate(watch("date1"));
return (
<DayPicker
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
if (!date) return;
onChange(date);
}}
mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/>
);
}}
/>
)}
</div>
@@ -7,6 +7,8 @@ import useToast from "hooks/use-toast";
import { usePopper } from "react-popper";
// ui
import { Button, Input } from "@plane/ui";
// icons
import { AlertCircle } from "lucide-react";
// components
import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
import { Popover, Transition } from "@headlessui/react";
@@ -250,8 +252,17 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
/>
)}
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
{responseActionButton}
<div className="flex gap-2 justify-between">
{responseActionButton ? (
<>{responseActionButton}</>
) : (
<>
<div className="flex items-center justify-center gap-2 text-sm text-custom-primary">
<AlertCircle className="h-4 w-4" />
<p>By using this feature, you consent to sharing the message with a 3rd party service. </p>
</div>
</>
)}
<div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Close
@@ -3,7 +3,7 @@ import { eachDayOfInterval, isValid } from "date-fns";
// ui
import { LineGraph } from "components/ui";
// helpers
import { renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import { getDate, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
//types
import { TCompletionChartDistribution } from "@plane/types";
@@ -47,11 +47,11 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
}));
const generateXAxisTickValues = () => {
const start = new Date(startDate);
const end = new Date(endDate);
const start = getDate(startDate);
const end = getDate(endDate);
let dates: Date[] = [];
if (isValid(start) && isValid(end)) {
if (start && end && isValid(start) && isValid(end)) {
dates = eachDayOfInterval({ start, end });
}
+10 -7
View File
@@ -27,7 +27,12 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers
import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import {
renderFormattedDate,
findHowManyDaysLeft,
renderFormattedDateWithoutYear,
getDate,
} from "helpers/date-time.helper";
import { truncateText } from "helpers/string.helper";
// types
import { ICycle, TCycleGroups } from "@plane/types";
@@ -102,8 +107,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
/>
);
const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? "");
const endDate = getDate(activeCycle.end_date);
const startDate = getDate(activeCycle.start_date);
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
const groupedIssues: any = {
backlog: activeCycle.backlog_issues,
@@ -113,8 +120,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cancelled: activeCycle.cancelled_issues,
};
const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
@@ -151,8 +156,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
color: group.color,
}));
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
return (
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
+3 -3
View File
@@ -12,7 +12,7 @@ import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon }
// icons
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
// helpers
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// constants
import { CYCLE_STATUS } from "constants/cycle";
@@ -50,8 +50,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const cycleStatus = cycleDetails.status.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const isDateValid = cycleDetails.start_date || cycleDetails.end_date;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
+3 -3
View File
@@ -12,7 +12,7 @@ import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarG
// icons
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
// helpers
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// constants
import { CYCLE_STATUS } from "constants/cycle";
@@ -137,8 +137,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
// TODO: change this logic once backend fix the response
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
+3 -3
View File
@@ -5,7 +5,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns";
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
@@ -135,8 +135,8 @@ export const CycleForm: React.FC<Props> = (props) => {
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -8,6 +8,7 @@ import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/
import { CycleGanttBlock } from "components/cycles";
// types
import { ICycle } from "@plane/types";
import { getDate } from "helpers/date-time.helper";
// constants
import { EUserProjectRoles } from "constants/project";
@@ -45,8 +46,8 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
data: block,
id: block?.id ?? "",
sort_order: block?.sort_order ?? 0,
start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block?.end_date ?? ""),
start_date: getDate(block?.start_date),
target_date: getDate(block?.end_date),
}));
return structuredBlocks;
+4 -4
View File
@@ -96,10 +96,10 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
};
const dateChecker = async (payload: CycleDateCheckData) => {
const dateChecker = async (projectId: string, payload: CycleDateCheckData) => {
let status = false;
await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload).then((res) => {
await cycleService.cycleDateCheck(workspaceSlug, projectId, payload).then((res) => {
status = res.status;
});
@@ -117,13 +117,13 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
if (payload.start_date && payload.end_date) {
if (data?.start_date && data?.end_date)
isDateValid = await dateChecker({
isDateValid = await dateChecker(payload.project_id ?? projectId, {
start_date: payload.start_date,
end_date: payload.end_date,
cycle_id: data.id,
});
else
isDateValid = await dateChecker({
isDateValid = await dateChecker(payload.project_id ?? projectId, {
start_date: payload.start_date,
end_date: payload.end_date,
});
+8 -5
View File
@@ -18,8 +18,8 @@ import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui";
// icons
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
// constants
@@ -186,8 +186,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const startDate = getDate(cycleDetails?.start_date);
const endDate = getDate(cycleDetails?.end_date);
const isStartValid = startDate && startDate <= new Date();
const isEndValid = endDate && startDate && endDate >= startDate;
const progressPercentage = cycleDetails
? isCompleted && cycleDetails?.progress_snapshot
@@ -317,8 +320,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -5,7 +5,7 @@ import { useIssueDetail, useMember, useProject } from "hooks/store";
// ui
import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui";
// helpers
import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper";
import { findTotalDaysInRange, getDate, renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue, TWidgetIssue } from "@plane/types";
@@ -34,6 +34,8 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const targetDate = getDate(issueDetails.target_date);
return (
<ControlLink
href={`/${workspaceSlug}/projects/${issueDetails.project_id}/issues/${issueDetails.id}`}
@@ -48,11 +50,7 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
<h6 className="text-sm flex-grow truncate">{issueDetails.name}</h6>
</div>
<div className="text-xs text-center">
{issueDetails.target_date
? isToday(new Date(issueDetails.target_date))
? "Today"
: renderFormattedDate(issueDetails.target_date)
: "-"}
{targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"}
</div>
<div className="text-xs text-center">
{blockedByIssues.length > 0
@@ -83,7 +81,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0;
return (
<ControlLink
@@ -212,7 +210,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
const projectDetails = getProjectById(issue.project_id);
const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false) ?? 0;
const dueBy = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0;
return (
<ControlLink
+3 -3
View File
@@ -9,7 +9,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { DropdownButton } from "./buttons";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TDropdownProps } from "./types";
@@ -168,8 +168,8 @@ export const DateDropdown: React.FC<Props> = (props) => {
{...attributes.popper}
>
<DayPicker
selected={value ? new Date(value) : undefined}
defaultMonth={value ? new Date(value) : undefined}
selected={getDate(value)}
defaultMonth={getDate(value)}
onSelect={(date) => {
dropdownOnChange(date ?? null);
}}
+3 -2
View File
@@ -2,7 +2,7 @@ import { useState, FC } from "react";
// ui
import { Button } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// types
import { IExportData } from "@plane/types";
@@ -18,7 +18,8 @@ export const SingleExport: FC<Props> = ({ service, refreshing }) => {
const checkExpiry = (inputDateString: string) => {
const currentDate = new Date();
const expiryDate = new Date(inputDateString);
const expiryDate = getDate(inputDateString);
if (!expiryDate) return false;
expiryDate.setDate(expiryDate.getDate() + 7);
return expiryDate > currentDate;
};
+2 -2
View File
@@ -64,9 +64,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
useGanttChart();
// rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
const renderBlockStructure = (view: ChartDataType, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: any) => ({
? blocks.map((block: IGanttBlock) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
+2 -2
View File
@@ -6,8 +6,8 @@ export interface IGanttBlock {
width: number;
};
sort_order: number;
start_date: Date | null;
target_date: Date | null;
start_date: Date | undefined;
target_date: Date | undefined;
}
export interface IBlockUpdateData {
@@ -1,4 +1,5 @@
// types
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { ChartDataType, IGanttBlock } from "../types";
// data
import { weeks, months } from "../data";
@@ -167,15 +168,16 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType,
const { startDate } = chartData.data;
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
if (!itemStartDate || !itemTargetDate) return null;
if (!itemStartDate || !itemTargetDate) return;
startDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0);
itemTargetDate.setHours(0, 0, 0, 0);
// position code starts
const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime();
const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)));
const positionDaysDifference = findTotalDaysInRange(startDate, itemStartDate, false);
if (!positionDaysDifference) return;
scrollPosition = positionDaysDifference * chartData.data.width;
var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12;
+17 -11
View File
@@ -21,6 +21,8 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
import { ISSUE_DELETED } from "constants/event-tracker";
//helpers
import { getDate } from "helpers/date-time.helper";
type TInboxIssueActionsHeader = {
workspaceSlug: string;
@@ -171,11 +173,11 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const tomorrow = getDate(today);
tomorrow?.setDate(today.getDate() + 1);
useEffect(() => {
if (!issueStatus || !issueStatus.snoozed_till) return;
setDate(new Date(issueStatus.snoozed_till));
setDate(issueStatus.snoozed_till);
}, [issueStatus]);
if (!issueStatus || !issue || !inboxIssues) return <></>;
@@ -269,19 +271,23 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{({ close }) => (
<div className="flex h-full w-full flex-col gap-y-1">
<DayPicker
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
selected={getDate(date)}
defaultMonth={getDate(date)}
onSelect={(date) => {
if (!date) return;
setDate(date);
}}
mode="single"
className="border border-custom-border-200 rounded-md p-3"
disabled={[
{
before: tomorrow,
},
]}
disabled={
tomorrow
? [
{
before: tomorrow,
},
]
: undefined
}
/>
<Button
variant="primary"
@@ -289,7 +295,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
close();
inboxIssueOperations.updateInboxIssueStatus({
status: 0,
snoozed_till: new Date(date),
snoozed_till: date,
});
}}
>
+2 -2
View File
@@ -28,7 +28,7 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
if (!inboxIssueStatusDetail) return <></>;
const isSnoozedDatePassed =
inboxIssueDetail.status === 0 && new Date(inboxIssueDetail.snoozed_till ?? "") < new Date();
inboxIssueDetail.status === 0 && !!inboxIssueDetail.snoozed_till && inboxIssueDetail.snoozed_till < new Date();
return (
<div
@@ -46,7 +46,7 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
workspaceSlug,
projectId,
inboxIssueDetail.duplicate_to ?? "",
new Date(inboxIssueDetail.snoozed_till ?? "")
inboxIssueDetail.snoozed_till
)
) : (
<span>{inboxIssueStatusDetail.title}</span>
@@ -9,7 +9,7 @@ import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "c
// icons
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
// helper
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
type Props = {
workspaceSlug: string;
@@ -33,7 +33,7 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
const projectDetails = issue ? getProjectById(issue.project_id) : null;
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
@@ -28,12 +28,13 @@ import {
IssueLabel,
ArchiveIssueModal,
} from "components/issues";
// components
import { IssueSubscription } from "./subscription";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// icons
import { ArchiveIcon, ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, Tooltip, UserGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { cn } from "helpers/common.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
@@ -99,10 +100,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const isInArchivableGroup =
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = issue.target_date ? getDate(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
return (
@@ -12,6 +12,8 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssuesFilter } from "store/issue/project-views";
// helpers
import { getDate } from "helpers/date-time.helper";
interface Props {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
@@ -47,8 +49,10 @@ export const CalendarMonthsDropdown: React.FC<Props> = observer((props: Props) =
const daysList = Object.keys(allDaysOfActiveWeek);
const firstDay = new Date(daysList[0]);
const lastDay = new Date(daysList[daysList.length - 1]);
const firstDay = getDate(daysList[0]);
const lastDay = getDate(daysList[daysList.length - 1]);
if (!firstDay || !lastDay) return "Week view";
if (firstDay.getMonth() === lastDay.getMonth() && firstDay.getFullYear() === lastDay.getFullYear())
return `${MONTHS_LIST[firstDay.getMonth() + 1].title} ${firstDay.getFullYear()}`;
@@ -58,6 +58,7 @@ export interface IGroupByKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props;
const member = useMember();
@@ -97,17 +99,27 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0);
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) => {
const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
if (sub_group_by) {
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true;
return false;
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
} else {
if (kanbanFilters?.group_by.includes(_list.id)) return true;
return false;
const groupVisibility = {
showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
else groupVisibility.showGroup = false;
}
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
return groupVisibility;
}
};
@@ -115,13 +127,18 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
return (
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{groupList &&
groupList.length > 0 &&
groupList.map((_list: IGroupByColumn) => {
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list);
if (groupByVisibilityToggle.showGroup === false) return <></>;
return (
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[350px]`}`}>
<div
className={`relative flex flex-shrink-0 flex-col group ${
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
} `}
>
{sub_group_by === null && (
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
<HeaderGroupByCard
@@ -141,7 +158,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div>
)}
{!groupByVisibilityToggle && (
{groupByVisibilityToggle.showIssues && (
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
@@ -159,7 +176,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
@@ -197,6 +213,7 @@ export interface IKanBan {
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
@@ -221,6 +238,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef,
isDragStarted,
showEmptyGroup,
subGroupIssueHeaderCount,
} = props;
const issueKanBanView = useKanbanView();
@@ -248,6 +266,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/>
);
});
@@ -37,7 +37,7 @@ interface IKanbanGroup {
viewId?: string;
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean;
groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
@@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
list: IGroupByColumn[];
kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
showEmptyGroup: boolean;
}
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
return headerCount;
};
const visibilitySubGroupByGroupCount = (
issueIds: TSubGroupedIssues,
_list: IGroupByColumn,
showEmptyGroup: boolean
): boolean => {
let subGroupHeaderVisibility = true;
if (showEmptyGroup) subGroupHeaderVisibility = true;
else {
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
else subGroupHeaderVisibility = false;
}
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds,
sub_group_by,
@@ -46,25 +63,36 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
list,
kanbanFilters,
handleKanbanFilters,
showEmptyGroup,
}) => (
<div className="relative flex gap-2 h-max min-h-full w-full items-center">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
/>
</div>
))}
list.map((_list: IGroupByColumn) => {
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
issueIds as TSubGroupedIssues,
_list,
showEmptyGroup
);
if (subGroupByVisibilityToggle === false) return <></>;
return (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
/>
</div>
);
})}
</div>
);
@@ -124,52 +152,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
return issueCount;
};
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const subGroupVisibility = {
showGroup: true,
showIssues: true,
};
if (showEmptyGroup) subGroupVisibility.showGroup = true;
else {
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
else subGroupVisibility.showGroup = false;
}
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
return subGroupVisibility;
};
return (
<div className="relative h-max min-h-full w-full">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
list.map((_list: any) => {
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
return (
<div className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
)}
</div>
))}
{subGroupByVisibilityToggle.showIssues && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}
/>
</div>
)}
</div>
);
})}
</div>
);
});
@@ -261,6 +311,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
list={groupByList}
showEmptyGroup={showEmptyGroup}
/>
</div>
@@ -19,7 +19,7 @@ import {
StateDropdown,
} from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { cn } from "helpers/common.helper";
// types
@@ -229,10 +229,10 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
@@ -284,7 +284,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue.start_date ?? null}
onChange={handleStartDate}
maxDate={maxDate ?? undefined}
maxDate={maxDate}
placeholder="Start date"
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
@@ -300,7 +300,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<DateDropdown
value={issue?.target_date ?? null}
onChange={handleTargetDate}
minDate={minDate ?? undefined}
minDate={minDate}
placeholder="Due date"
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
@@ -6,9 +6,9 @@ import { useProjectState } from "hooks/store";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
import { cn } from "helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
// types
import { TIssue } from "@plane/types";
@@ -30,7 +30,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.target_date}
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
minDate={getDate(issue.start_date)}
onChange={(data) => {
const targetDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
@@ -4,7 +4,7 @@ import { CalendarClock } from "lucide-react";
// components
import { DateDropdown } from "components/dropdowns";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
@@ -22,7 +22,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
<div className="h-11 border-b-[0.5px] border-custom-border-200">
<DateDropdown
value={issue.start_date}
maxDate={issue.target_date ? new Date(issue.target_date) : undefined}
maxDate={getDate(issue.target_date)}
onChange={(data) => {
const startDate = data ? renderFormattedPayloadDate(data) : null;
onChange(
+4 -4
View File
@@ -26,10 +26,10 @@ import {
MemberDropdown,
StateDropdown,
} from "components/dropdowns";
// ui
//ui
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import type { TIssue, ISearchIssueResponse } from "@plane/types";
@@ -236,10 +236,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
const minDate = getDate(startDate);
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
const maxDate = getDate(targetDate);
maxDate?.setDate(maxDate.getDate());
const projectDetails = getProjectById(projectId);
@@ -26,9 +26,9 @@ import {
} from "components/issues";
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
// components
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// helpers
import { cn } from "helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
interface IPeekOverviewProperties {
@@ -54,10 +54,10 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const isEstimateEnabled = projectDetails?.estimate;
const stateDetails = getStateById(issue.state_id);
const minDate = issue.start_date ? new Date(issue.start_date) : null;
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
+3 -3
View File
@@ -6,7 +6,7 @@ import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/d
// ui
import { Button, Input, TextArea } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IModule } from "@plane/types";
@@ -147,8 +147,8 @@ export const ModuleForm: React.FC<Props> = (props) => {
className="h-7"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
@@ -7,6 +7,7 @@ import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components
import { ModuleGanttBlock } from "components/modules";
// types
import { IModule } from "@plane/types";
import { getDate } from "helpers/date-time.helper";
export const ModulesListGanttChartView: React.FC = observer(() => {
// router
@@ -32,8 +33,8 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
start_date: getDate(block.start_date),
target_date: getDate(block.target_date),
};
});
+3 -3
View File
@@ -12,7 +12,7 @@ import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// constants
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
@@ -135,8 +135,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const completionPercentage = (moduleDetails.completed_issues / moduleTotalIssues) * 100;
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(moduleDetails.start_date ?? "");
const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const isDateValid = moduleDetails.target_date || moduleDetails.start_date;
+3 -3
View File
@@ -12,7 +12,7 @@ import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules";
import { Avatar, AvatarGroup, CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { getDate, renderFormattedDate } from "helpers/date-time.helper";
// constants
import { MODULE_STATUS } from "constants/module";
import { EUserProjectRoles } from "constants/project";
@@ -129,8 +129,8 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
const completionPercentage =
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(moduleDetails.start_date ?? "");
const endDate = getDate(moduleDetails.target_date);
const startDate = getDate(moduleDetails.start_date);
const renderDate = moduleDetails.start_date || moduleDetails.target_date;
+29 -23
View File
@@ -25,7 +25,7 @@ import { DateRangeDropdown, MemberDropdown } from "components/dropdowns";
// ui
import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
@@ -201,8 +201,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
});
}, [moduleDetails, reset]);
const isStartValid = new Date(`${moduleDetails?.start_date}`) <= new Date();
const isEndValid = new Date(`${moduleDetails?.target_date}`) >= new Date(`${moduleDetails?.start_date}`);
const startDate = getDate(moduleDetails?.start_date);
const endDate = getDate(moduleDetails?.target_date);
const isStartValid = startDate && startDate <= new Date();
const isEndValid = startDate && endDate && endDate >= startDate;
const progressPercentage = moduleDetails
? Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
@@ -342,26 +344,30 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Controller
control={control}
name="target_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDateValue ? new Date(startDateValue) : undefined,
to: endDateValue ? new Date(endDateValue) : undefined,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "Target date",
}}
/>
)}
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => {
const startDate = getDate(startDateValue);
const endDate = getDate(endDateValue);
return (
<DateRangeDropdown
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: startDate,
to: endDate,
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "Target date",
}}
/>
);
}}
/>
)}
/>
@@ -13,7 +13,7 @@ import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
import { snoozeOptions } from "constants/notification";
// helper
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "helpers/date-time.helper";
// type
import type { IUserNotification, NotificationType } from "@plane/types";
// constants
@@ -119,7 +119,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const notificationField = notification.data.issue_activity.field;
const notificationTriggeredBy = notification.triggered_by_details;
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
const snoozedTillDate = getDate(notification?.snoozed_till);
if (snoozedTillDate && isSnoozedTabOpen && snoozedTillDate < new Date()) return null;
return (
<Link
@@ -12,6 +12,8 @@ import useToast from "hooks/use-toast";
import { Button, CustomSelect } from "@plane/ui";
// types
import type { IUserNotification } from "@plane/types";
// helpers
import { getDate } from "helpers/date-time.helper";
type SnoozeModalProps = {
isOpen: boolean;
@@ -60,7 +62,7 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
if (!formDataDate) return timeStamps;
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
const isToday = today.toDateString() === getDate(formDataDate)?.toDateString();
if (!isToday) return timeStamps;
@@ -93,9 +95,9 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
);
const minutes = parseInt(time[1]);
const dateTime = new Date(formData.date);
dateTime.setHours(hours);
dateTime.setMinutes(minutes);
const dateTime: Date | undefined = getDate(formData?.date);
dateTime?.setHours(hours);
dateTime?.setMinutes(minutes);
await handleSubmitSnooze(notification.id, dateTime).then(() => {
handleClose();
@@ -214,10 +216,11 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "AM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "AM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
AM
</div>
@@ -225,10 +228,11 @@ export const SnoozeNotificationModal: FC<SnoozeModalProps> = (props) => {
onClick={() => {
setValue("period", "PM");
}}
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM"
className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${
watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
}`}
>
PM
</div>
+6 -1
View File
@@ -8,7 +8,12 @@ export const INBOX_STATUS: {
status: number;
icon: LucideIcon;
title: string;
description: (workspaceSlug: string, projectId: string, issueId: string, snoozedTillDate: Date) => JSX.Element;
description: (
workspaceSlug: string,
projectId: string,
issueId: string,
snoozedTillDate: Date | undefined
) => JSX.Element;
textColor: (snoozeDatePassed: boolean) => string;
bgColor: (snoozeDatePassed: boolean) => string;
borderColor: (snoozeDatePassed: boolean) => string;
+2 -2
View File
@@ -91,7 +91,7 @@ const UserNotificationContextProvider: React.FC<{
const [state, dispatch] = useReducer(reducer, initialState);
const { selectedTab, snoozed, archived, readNotification, selectedNotificationForSnooze } = state;
const { selectedTab, snoozed, archived, readNotification } = state;
const params = {
type: snoozed || archived || readNotification ? undefined : selectedTab,
@@ -207,7 +207,7 @@ const UserNotificationContextProvider: React.FC<{
(previousNotifications: any) =>
previousNotifications?.map((notification: any) =>
notification.id === notificationId
? { ...notification, snoozed_till: isSnoozed ? null : new Date(dateTime!) }
? { ...notification, snoozed_till: isSnoozed ? null : dateTime }
: notification
) || [],
false
+48 -19
View File
@@ -1,5 +1,5 @@
import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns";
import { isNil } from "lodash";
import isNumber from "lodash/isNumber";
// Format Date Helpers
/**
@@ -8,10 +8,11 @@ import { isNil } from "lodash";
* @param {Date | string} date
* @example renderFormattedDate("2024-01-01") // Jan 01, 2024
*/
export const renderFormattedDate = (date: string | Date): string | null => {
if (!date) return null;
export const renderFormattedDate = (date: string | Date | undefined | null): string | null => {
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return null;
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return null; // Return null for invalid dates
// Format the date in format (MMM dd, yyyy)
@@ -26,9 +27,10 @@ export const renderFormattedDate = (date: string | Date): string | null => {
* @example renderShortDateFormat("2024-01-01") // Jan 01
*/
export const renderFormattedDateWithoutYear = (date: string | Date): string => {
if (!date) return "";
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return "";
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
// Format the date in short format (MMM dd)
@@ -43,9 +45,10 @@ export const renderFormattedDateWithoutYear = (date: string | Date): string => {
* @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01"
*/
export const renderFormattedPayloadDate = (date: Date | string): string | null => {
if (!date) return null;
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return null;
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return null; // Return null for invalid dates
// Format the date in payload format (yyyy-mm-dd)
@@ -62,10 +65,14 @@ export const renderFormattedPayloadDate = (date: Date | string): string | null =
* @example renderFormattedTime("2024-01-01 13:00:00") // 13:00
* @example renderFormattedTime("2024-01-01 13:00:00", "12-hour") // 01:00 PM
*/
export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => {
if (!date || date === "") return "";
export const renderFormattedTime = (
date: string | Date | undefined | null,
timeFormat: "12-hour" | "24-hour" = "24-hour"
): string => {
// Parse the date to check if it is valid
const parsedDate = new Date(date);
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return "";
// Check if the parsed date is valid
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
// Format the date in 12 hour format if in12HourFormat is true
@@ -92,10 +99,11 @@ export const findTotalDaysInRange = (
endDate: Date | string | undefined | null,
inclusive: boolean = true
): number | undefined => {
if (!startDate || !endDate) return undefined;
// Parse the dates to check if they are valid
const parsedStartDate = new Date(startDate);
const parsedEndDate = new Date(endDate);
const parsedStartDate = getDate(startDate);
const parsedEndDate = getDate(endDate);
// return if undefined
if (!parsedStartDate || !parsedEndDate) return;
// Check if the parsed dates are valid before calculating the difference
if (!isValid(parsedStartDate) || !isValid(parsedEndDate)) return 0; // Return 0 for invalid dates
// Calculate the difference in days
@@ -131,6 +139,7 @@ export const calculateTimeAgo = (time: string | number | Date | null): string =>
if (!time) return "";
// Parse the time to check if it is valid
const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time;
// return if undefined
if (!parsedTime) return ""; // Return empty string for invalid dates
// Format the time in the form of amount of time passed since the event happened
const distance = formatDistanceToNow(parsedTime, { addSuffix: true });
@@ -164,7 +173,7 @@ export const isDateGreaterThanToday = (dateStr: string): boolean => {
* @example getWeekNumber(new Date("2023-09-01")) // 35
*/
export const getWeekNumberOfDate = (date: Date): number => {
const currentDate = new Date(date);
const currentDate = date;
// Adjust the starting day to Sunday (0) instead of Monday (1)
const startDate = new Date(currentDate.getFullYear(), 0, 1);
// Calculate the number of days between currentDate and startDate
@@ -186,10 +195,30 @@ export const checkIfDatesAreEqual = (
date1: Date | string | null | undefined,
date2: Date | string | null | undefined
): boolean => {
if (isNil(date1) && isNil(date2)) return true;
if (isNil(date1) || isNil(date2)) return false;
const parsedDate1 = getDate(date1);
const parsedDate2 = getDate(date2);
// return if undefined
if (!parsedDate1 && !parsedDate2) return true;
if (!parsedDate1 || !parsedDate2) return false;
const parsedDate1 = new Date(date1);
const parsedDate2 = new Date(date2);
return isEqual(parsedDate1, parsedDate2);
};
/**
* This method returns a date from string of type yyyy-mm-dd
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
* @param date
* @returns date or undefined
*/
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
if (!date || date === "") return;
if (typeof date !== "string" && !(date instanceof String)) return date;
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
const year = parseInt(yearString);
const month = parseInt(monthString);
const day = parseInt(dayString);
if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return;
return new Date(year, month - 1, day);
};
+34
View File
@@ -1,5 +1,7 @@
// types
import { IIssueFilterOptions } from "@plane/types";
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import { getDate } from "./date-time.helper";
export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
filters && Object.keys(filters).length > 0
@@ -14,3 +16,35 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
.reduce((curr, prev) => curr + prev, 0)
: 0;
/**
* @description checks if the date satisfies the filter
* @param {Date} date
* @param {string} filter
* @returns {boolean}
*/
export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
const [value, operator, from] = filter.split(";");
const dateValue = getDate(value);
if (!from && dateValue) {
if (operator === "after") return date >= dateValue;
if (operator === "before") return date <= dateValue;
}
if (from === "fromnow") {
if (operator === "before") {
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) <= -7;
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) <= -14;
if (value === "1_months") return differenceInCalendarDays(date, new Date()) <= -30;
}
if (operator === "after") {
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) >= 7;
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) >= 14;
if (value === "1_months") return differenceInCalendarDays(date, new Date()) >= 30;
if (value === "2_months") return differenceInCalendarDays(date, new Date()) >= 60;
}
}
return false;
};
+6 -3
View File
@@ -1,6 +1,7 @@
import { v4 as uuidv4 } from "uuid";
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
// helpers
import { getDate } from "./date-time.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import {
@@ -157,7 +158,9 @@ export const shouldHighlightIssueDueDate = (
// if the issue is completed or cancelled, don't highlight the due date
if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false;
const parsedDate = new Date(date);
const parsedDate = getDate(date);
if (!parsedDate) return false;
const targetDateDistance = differenceInCalendarDays(parsedDate, new Date());
// if the issue is overdue, highlight the due date
@@ -168,6 +171,6 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
start_date: getDate(block.start_date),
target_date: getDate(block.target_date),
}));
@@ -21,6 +21,7 @@ import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/doc
import { Spinner } from "@plane/ui";
// assets
// helpers
import { getDate } from "helpers/date-time.helper";
// types
import { IPage } from "@plane/types";
import { NextPageWithLayout } from "lib/types";
@@ -271,8 +272,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
@@ -282,7 +283,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: archived_at ? new Date(archived_at) : undefined,
archived_at: getDate(archived_at),
}
: undefined
}
@@ -298,8 +299,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
created_on: getDate(created_at) ?? new Date(created_at ?? ""),
last_updated_at: getDate(updated_at) ?? new Date(created_at ?? ""),
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
+11 -4
View File
@@ -4,6 +4,10 @@ import { isFuture, isPast, isToday } from "date-fns";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
// types
// helpers
import { getDate } from "helpers/date-time.helper";
// mobx
// services
import { ICycle, CycleDateCheckData } from "@plane/types";
// mobx
import { RootStore } from "store/root.store";
@@ -118,8 +122,9 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
const isEndDateToday = isToday(new Date(c.end_date ?? ""));
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
const isEndDateToday = endDate && isToday(endDate);
return c.project_id === projectId && hasEndDatePassed && !isEndDateToday;
});
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
@@ -134,7 +139,8 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
const startDate = getDate(c.start_date);
const isStartDateUpcoming = startDate && isFuture(startDate);
return c.project_id === projectId && isStartDateUpcoming;
});
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
@@ -149,7 +155,8 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
return c.project_id === projectId && !hasEndDatePassed;
});
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
+38 -35
View File
@@ -1,16 +1,17 @@
import orderBy from "lodash/orderBy";
import get from "lodash/get";
import indexOf from "lodash/indexOf";
import isEmpty from "lodash/isEmpty";
import orderBy from "lodash/orderBy";
import values from "lodash/values";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
import { IIssueRootStore } from "../root.store";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
import { STATE_GROUPS } from "constants/state";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
// store
import { IIssueRootStore } from "../root.store";
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
@@ -62,35 +63,35 @@ export class IssueHelperStore implements TIssueHelperStore {
issues: TIssueMap,
isCalendarIssues: boolean = false
) => {
const _issues: { [group_id: string]: string[] } = {};
if (!groupBy) return _issues;
const currentIssues: { [group_id: string]: string[] } = {};
if (!groupBy) return currentIssues;
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
_issues[group] = [];
currentIssues[group] = [];
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const _issue = projectIssues[issue];
const currentIssue = projectIssues[issue];
let groupArray = [];
if (groupBy === "state_detail.group") {
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
// if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap
const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None";
groupArray = [state_group];
} else {
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : [];
const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"];
}
for (const group of groupArray) {
if (group && _issues[group]) _issues[group].push(_issue.id);
else if (group) _issues[group] = [_issue.id];
if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id);
else if (group) currentIssues[group] = [currentIssue.id];
}
}
return _issues;
return currentIssues;
};
subGroupedIssues = (
@@ -99,45 +100,47 @@ export class IssueHelperStore implements TIssueHelperStore {
orderBy: TIssueOrderByOptions,
issues: TIssueMap
) => {
const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return _issues;
const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {};
if (!subGroupBy || !groupBy) return currentIssues;
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => {
this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => {
const groupByIssues: { [group_id: string]: string[] } = {};
this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => {
groupByIssues[group] = [];
});
_issues[sub_group] = groupByIssues;
currentIssues[sub_group] = groupByIssues;
});
const projectIssues = this.issuesSortWithOrderBy(issues, orderBy);
for (const issue in projectIssues) {
const _issue = projectIssues[issue];
const currentIssue = projectIssues[issue];
let subGroupArray = [];
let groupArray = [];
if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") {
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None";
subGroupArray = [state_group];
groupArray = [state_group];
} else {
const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]);
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : [];
groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : [];
const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]);
const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"];
groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"];
}
for (const subGroup of subGroupArray) {
for (const group of groupArray) {
if (subGroup && group && _issues?.[subGroup]?.[group]) _issues[subGroup][group].push(_issue.id);
else if (subGroup && group && _issues[subGroup]) _issues[subGroup][group] = [_issue.id];
else if (subGroup && group) _issues[subGroup] = { [group]: [_issue.id] };
if (subGroup && group && currentIssues?.[subGroup]?.[group])
currentIssues[subGroup][group].push(currentIssue.id);
else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group] = [currentIssue.id];
else if (subGroup && group) currentIssues[subGroup] = { [group]: [currentIssue.id] };
}
}
}
return _issues;
return currentIssues;
};
unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) =>
@@ -215,8 +218,8 @@ export class IssueHelperStore implements TIssueHelperStore {
const moduleMap = this.rootStore?.moduleMap;
if (!moduleMap) break;
for (const dataId of dataIdsArray) {
const _module = moduleMap[dataId];
if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase());
const currentModule = moduleMap[dataId];
if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase());
}
break;
case "cycle_id":
@@ -233,10 +236,10 @@ export class IssueHelperStore implements TIssueHelperStore {
}
/**
* This Method is mainly used to filter out empty values in the begining
* This Method is mainly used to filter out empty values in the beginning
* @param key key of the value that is to be checked if empty
* @param object any object in which the key's value is to be checked
* @returns 1 if emoty, 0 if not empty
* @returns 1 if empty, 0 if not empty
*/
getSortOrderToFilterEmptyValues(key: string, object: any) {
const value = object?.[key];
@@ -388,7 +391,7 @@ export class IssueHelperStore implements TIssueHelperStore {
getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] {
if (!value || value === null || value === undefined) return ["None"];
if (Array.isArray(value))
if (value.length) return value;
if (value && value.length) return value;
else return ["None"];
else if (typeof value === "boolean") return [value ? "True" : "False"];
else if (typeof value === "number") return [value.toString()];
+7 -7
View File
@@ -10,7 +10,7 @@ export interface IPageStore {
access: number;
archived_at: string | null;
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html: string;
@@ -23,7 +23,7 @@ export interface IPageStore {
name: string;
owned_by: string;
project: string;
updated_at: Date;
updated_at: string | null;
updated_by: string;
workspace: string;
@@ -52,7 +52,7 @@ export class PageStore implements IPageStore {
isSubmitting: "submitting" | "submitted" | "saved" = "saved";
archived_at: string | null;
color: string;
created_at: Date;
created_at: string | null;
created_by: string;
description: string;
description_html = "";
@@ -64,7 +64,7 @@ export class PageStore implements IPageStore {
name = "";
owned_by: string;
project: string;
updated_at: Date;
updated_at: string | null;
updated_by: string;
workspace: string;
oldName = "";
@@ -94,9 +94,9 @@ export class PageStore implements IPageStore {
cleanup: action,
});
this.created_by = page?.created_by || "";
this.created_at = page?.created_at || new Date();
this.created_at = page?.created_at ?? "";
this.color = page?.color || "";
this.archived_at = page?.archived_at || null;
this.archived_at = page?.archived_at ?? null;
this.name = page?.name || "";
this.description = page?.description || "";
this.description_stripped = page?.description_stripped || "";
@@ -104,7 +104,7 @@ export class PageStore implements IPageStore {
this.access = page?.access || 0;
this.workspace = page?.workspace || "";
this.updated_by = page?.updated_by || "";
this.updated_at = page?.updated_at || new Date();
this.updated_at = page?.updated_at ?? "";
this.project = page?.project || "";
this.owned_by = page?.owned_by || "";
this.labels = page?.labels || [];
+17 -18
View File
@@ -8,6 +8,7 @@ import { PageStore, IPageStore } from "store/page.store";
import { IPage, IRecentPages } from "@plane/types";
import { RootStore } from "./root.store";
import { isThisWeek, isToday, isYesterday } from "date-fns";
import { getDate } from "helpers/date-time.helper";
export interface IProjectPageStore {
loader: boolean;
@@ -73,8 +74,8 @@ export class ProjectPageStore implements IProjectPageStore {
const allProjectIds = Object.keys(this.projectPageMap[projectId]);
return allProjectIds.sort((a, b) => {
const dateA = new Date(this.projectPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectPageMap[projectId][b].created_at).getTime();
const dateA = getDate(this.projectPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
return dateB - dateA;
});
}
@@ -84,8 +85,8 @@ export class ProjectPageStore implements IProjectPageStore {
if (!projectId || !this.projectArchivedPageMap[projectId]) return [];
const archivedPages = Object.keys(this.projectArchivedPageMap[projectId]);
return archivedPages.sort((a, b) => {
const dateA = new Date(this.projectArchivedPageMap[projectId][a].created_at).getTime();
const dateB = new Date(this.projectArchivedPageMap[projectId][b].created_at).getTime();
const dateA = getDate(this.projectArchivedPageMap[projectId]?.[a]?.created_at)?.getTime() ?? 0;
const dateB = getDate(this.projectArchivedPageMap[projectId]?.[b]?.created_at)?.getTime() ?? 0;
return dateB - dateA;
});
}
@@ -126,26 +127,24 @@ export class ProjectPageStore implements IProjectPageStore {
const projectId = this.rootStore.app.router.projectId;
if (!this.projectPageIds || !projectId) return;
const today: string[] = this.projectPageIds.filter((page) =>
isToday(new Date(this.projectPageMap[projectId][page].updated_at))
);
const today: string[] = this.projectPageIds.filter((page) => {
const updatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return updatedAt && isToday(updatedAt);
});
const yesterday: string[] = this.projectPageIds.filter((page) =>
isYesterday(new Date(this.projectPageMap[projectId][page].updated_at))
);
const yesterday: string[] = this.projectPageIds.filter((page) => {
const updatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return updatedAt && isYesterday(updatedAt);
});
const this_week: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return (
isThisWeek(new Date(pageUpdatedAt)) &&
!isToday(new Date(pageUpdatedAt)) &&
!isYesterday(new Date(pageUpdatedAt))
);
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && isThisWeek(pageUpdatedAt) && !isToday(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
});
const older: string[] = this.projectPageIds.filter((page) => {
const pageUpdatedAt = this.projectPageMap[projectId][page].updated_at;
return !isThisWeek(new Date(pageUpdatedAt)) && !isYesterday(new Date(pageUpdatedAt));
const pageUpdatedAt = getDate(this.projectPageMap[projectId]?.[page]?.updated_at);
return pageUpdatedAt && !isThisWeek(pageUpdatedAt) && !isYesterday(pageUpdatedAt);
});
return { today, yesterday, this_week, older };