From 59c9b3bdcea56640c95f7d4df0cbeddd9e378544 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 4 Mar 2024 15:13:39 +0530 Subject: [PATCH 001/214] chore: added auto merge CI for merging sync branches --- .github/workflows/auto-merge.yml | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/auto-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 00000000000..60ebe583418 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,97 @@ +name: Auto Merge or Create PR on Push + +on: + workflow_dispatch: + push: + branches: + - "sync/**" + +env: + CURRENT_BRANCH: ${{ github.ref_name }} + SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce" + TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows + REVIEWER: ${{ secrets.REVIEWER }} + +jobs: + Check_Branch: + runs-on: ubuntu-latest + outputs: + BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }} + steps: + - name: Check if current branch matches the secret + id: check-branch + run: | + if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then + echo "MATCH=true" >> $GITHUB_OUTPUT + else + echo "MATCH=false" >> $GITHUB_OUTPUT + fi + + Auto_Merge: + if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }} + needs: [Check_Branch] + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Setup GH CLI and Git Config + run: | + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + + - id: git-author + name: Setup Git CLI from Github Token + run: | + VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer') + VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}") + VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}") + VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}") + + USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}" + USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com" + + git config --global user.name ${USER_NAME} + git config --global user.email ${USER_EMAIL} + + - name: Check for merge conflicts + id: conflicts + run: | + git fetch origin $TARGET_BRANCH + git checkout $TARGET_BRANCH + # Attempt to merge the main branch into the current branch + if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then + echo "No merge conflicts detected." + echo "HAS_CONFLICTS=false" >> $GITHUB_ENV + else + echo "Merge conflicts detected." + echo "HAS_CONFLICTS=true" >> $GITHUB_ENV + git merge --abort + fi + + - name: Merge Change to Target Branch + if: env.HAS_CONFLICTS == 'false' + run: | + git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH" + git push origin $TARGET_BRANCH + + - name: Create PR to Target Branch + if: env.HAS_CONFLICTS == 'true' + run: | + # Use GitHub CLI to create PR and specify author and committer + PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \ + --title "sync: merge conflicts need to be resolved" \ + --body "" \ + --reviewer $REVIEWER ) + echo "Pull Request created: $PR_URL" + From af70722e34ceeb09cf2c1bee04a69ce89e7d7791 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 5 Mar 2024 13:02:13 +0530 Subject: [PATCH 002/214] chore: added workflow for checking version before merge to master (#3847) --- .github/workflows/check-version.yml | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/check-version.yml diff --git a/.github/workflows/check-version.yml b/.github/workflows/check-version.yml new file mode 100644 index 00000000000..ca8b6f8b3e0 --- /dev/null +++ b/.github/workflows/check-version.yml @@ -0,0 +1,45 @@ +name: Version Change Before Release + +on: + pull_request: + branches: + - master + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Get PR Branch version + run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Fetch base branch + run: git fetch origin master:master + + - name: Get Master Branch version + run: | + git checkout master + echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Get master branch version and compare + run: | + echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION" + if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then + echo "Version in PR branch is the same as in master. Failing the CI." + exit 1 + else + echo "Version check passed. Versions are different." + fi + env: + PR_VERSION: ${{ env.PR_VERSION }} + MASTER_VERSION: ${{ env.MASTER_VERSION }} From f8f9dd33311686e94c2386cfe94af14569662d90 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 5 Mar 2024 13:14:00 +0530 Subject: [PATCH 003/214] [CHANG-8] chore: Upgraded Build Pull Request CI for Faster Parallel Build with Linting Capabilities (#3838) * chore: upgraded build pull request ci for multi stage parallel builds * Update build-test-pull-request.yml --- .github/workflows/build-test-pull-request.yml | 111 +++++++++++++----- 1 file changed, 84 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml index 83ed41625df..e0014f696f4 100644 --- a/.github/workflows/build-test-pull-request.yml +++ b/.github/workflows/build-test-pull-request.yml @@ -1,27 +1,19 @@ -name: Build Pull Request Contents +name: Build and Lint on Pull Request on: + workflow_dispatch: pull_request: types: ["opened", "synchronize"] jobs: - build-pull-request-contents: - name: Build Pull Request Contents - runs-on: ubuntu-20.04 - permissions: - pull-requests: read - + get-changed-files: + runs-on: ubuntu-latest + outputs: + apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }} + web_changed: ${{ steps.changed-files.outputs.web_any_changed }} + space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }} steps: - - name: Checkout Repository to Actions - uses: actions/checkout@v3.3.0 - with: - token: ${{ secrets.ACCESS_TOKEN }} - - - name: Setup Node.js 18.x - uses: actions/setup-node@v2 - with: - node-version: 18.x - + - uses: actions/checkout@v3 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v41 @@ -31,17 +23,82 @@ jobs: - apiserver/** web: - web/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' deploy: - space/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' + + lint-apiserver: + needs: get-changed-files + runs-on: ubuntu-latest + if: needs.get-changed-files.outputs.apiserver_changed == 'true' + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version you need + - name: Install Pylint + run: python -m pip install ruff + - name: Install Apiserver Dependencies + run: cd apiserver && pip install -r requirements.txt + - name: Lint apiserver + run: ruff check --fix apiserver + + lint-web: + needs: get-changed-files + if: needs.get-changed-files.outputs.web_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=web - - name: Build Plane's Main App - if: steps.changed-files.outputs.web_any_changed == 'true' - run: | - yarn - yarn build --filter=web + lint-space: + needs: get-changed-files + if: needs.get-changed-files.outputs.space_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=space + + build-web: + needs: lint-web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=web - - name: Build Plane's Deploy App - if: steps.changed-files.outputs.deploy_any_changed == 'true' - run: | - yarn - yarn build --filter=space + build-space: + needs: lint-space + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=space From d07dd650222706aaf305b470bf2f6fee0c178b21 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:57:14 +0530 Subject: [PATCH 004/214] feat: feature preview deploys for web and space nextjs applications (#3881) * feature preview deploy * chore: variable name changes --------- Co-authored-by: sriram veeraghanta --- .github/workflows/feature-deployment.yml | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/feature-deployment.yml diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml new file mode 100644 index 00000000000..2220a7a846c --- /dev/null +++ b/.github/workflows/feature-deployment.yml @@ -0,0 +1,73 @@ +name: Feature Preview + +on: + workflow_dispatch: + inputs: + web-build: + required: true + type: boolean + default: true + space-build: + required: true + type: boolean + default: false + +jobs: + feature-deploy: + 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 }} + + steps: + - 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" + 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 }} + + 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.repositoryUrl }} \ + --set shared_config.git_branch="$GIT_BRANCH" \ + --set web.enabled=${{ env.BUILD_WEB }} \ + --set space.enabled=${{ env.BUILD_SPACE }} \ + --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 "****************************************" From 4d0f641ee0cae4fac344c6caaa44abc602442369 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 6 Mar 2024 14:02:14 +0530 Subject: [PATCH 005/214] [WEB-588] chore: remove the word `title` from the issue title tooltip. (#3874) * [WEB-588] chore: remove the word `title` from the issue title tooltip. * fix: github url fixes in feature deploy action --------- Co-authored-by: sriram veeraghanta --- .github/workflows/feature-deployment.yml | 2 +- web/components/cycles/active-cycle-details.tsx | 2 +- web/components/issues/issue-layouts/calendar/issue-blocks.tsx | 2 +- web/components/issues/issue-layouts/gantt/blocks.tsx | 2 +- web/components/issues/issue-layouts/kanban/block.tsx | 4 ++-- web/components/issues/issue-layouts/list/block.tsx | 4 ++-- web/components/issues/issue-layouts/spreadsheet/issue-row.tsx | 4 ++-- web/components/issues/sub-issues/issue-list-item.tsx | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 2220a7a846c..7b9f5ffcc4b 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -54,7 +54,7 @@ jobs: --kube-insecure-skip-tls-verify \ --generate-name \ --namespace $APP_NAMESPACE \ - --set shared_config.git_repo=${{ github.repositoryUrl }} \ + --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 }} \ diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 1fae0412f5b..425ce7df3fb 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -311,7 +311,7 @@ export const ActiveCycleDetails: React.FC = observer((props {currentProjectDetails?.identifier}-{issue.sequence_id} - + {truncateText(issue.name, 30)} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index b5d0c434609..ac60053726d 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -110,7 +110,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => {
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
- +
{issue.name}
diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index 209d876ac56..73031354918 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -97,7 +97,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => {
{projectIdentifier} {issueDetails?.sequence_id}
- + {issueDetails?.name} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index be27f77068a..602c6a9341b 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -71,7 +71,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -84,7 +84,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index cc04ed71620..90fee10cc73 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -65,7 +65,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock )} {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -78,7 +78,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 9f4810c7805..abf6c3a0149 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -241,9 +241,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { disabled={!!issueDetail?.tempId} >
- +
{issueDetail.name} diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index c6b87411d4b..a748e986e9b 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -117,7 +117,7 @@ export const IssueListItem: React.FC = observer((props) => { onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" > - + {issue.name} From c06ef4d1d77942d68a7f082f119712535312429a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:18:19 +0530 Subject: [PATCH 006/214] [WEB-579] style: scrollbar implementation (#3835) * style: scrollbar added in profile summary and sidebar * style: scrollbar added in modals * style: scrollbar added in project setting screens * style: scrollbar added in workspace and profile settings * style: scrollbar added in dropdowns and filters --- .../analytics/custom-analytics/main-content.tsx | 2 +- .../custom-analytics/sidebar/projects-list.tsx | 4 ++-- .../analytics/custom-analytics/sidebar/sidebar.tsx | 2 +- web/components/core/image-picker-popover.tsx | 2 +- .../core/modals/existing-issues-list-modal.tsx | 2 +- web/components/emoji-icon-picker/index.tsx | 2 +- .../display-filters/display-filters-selection.tsx | 2 +- .../filters/header/filters/filters-selection.tsx | 2 +- web/components/issues/parent-issues-list-modal.tsx | 5 ++++- web/components/profile/sidebar.tsx | 11 ++++++----- web/layouts/settings-layout/profile/layout.tsx | 4 +++- .../settings-layout/profile/preferences/layout.tsx | 4 +++- web/layouts/settings-layout/profile/sidebar.tsx | 4 ++-- web/layouts/settings-layout/project/layout.tsx | 4 +++- web/layouts/settings-layout/workspace/layout.tsx | 4 ++-- web/pages/[workspaceSlug]/profile/[userId]/index.tsx | 2 +- .../projects/[projectId]/settings/estimates.tsx | 2 +- .../projects/[projectId]/settings/integrations.tsx | 2 +- .../projects/[projectId]/settings/labels.tsx | 2 +- web/pages/[workspaceSlug]/settings/api-tokens.tsx | 2 +- web/pages/[workspaceSlug]/settings/webhooks/index.tsx | 2 +- web/pages/profile/activity.tsx | 2 +- web/pages/profile/index.tsx | 2 +- web/pages/profile/preferences/email.tsx | 2 +- 24 files changed, 41 insertions(+), 31 deletions(-) diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 3c199f8078c..7781e786962 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -33,7 +33,7 @@ export const CustomAnalyticsMainContent: React.FC = (props) => { {!error ? ( analytics ? ( analytics.total > 0 ? ( -
+
= observer((pro return (

Selected Projects

-
+
{projectIds.map((projectId) => { const project = getProjectById(projectId); @@ -42,7 +42,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro ({project.identifier})
-
+
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 3ad2805f28f..bf1c80fea79 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index b2e4c4c9fdb..09a1fd4e4d0 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -187,7 +187,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { ); })} - + {(unsplashImages || !unsplashError) && (
diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index c4fa25c6d6c..1b6a1e76bdc 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -184,7 +184,7 @@ export const ExistingIssuesListModal: React.FC = (props) => { )}
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 57d5d889672..9c45e535687 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -94,7 +94,7 @@ const EmojiIconPicker: React.FC = (props) => { ))} - + {recentEmojis.length > 0 && (
diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 131bea46bce..b8988580aaa 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -37,7 +37,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter); return ( -
+
{/* display properties */} {layoutDisplayFiltersOptions?.display_properties && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index afdee86f2cc..ae7ded8b2d2 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -63,7 +63,7 @@ export const FilterSelection: React.FC = observer((props) => { )}
-
+
{/* priority */} {isFilterEnabled("priority") && (
diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index c8520562e40..b97eafc0643 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -136,7 +136,10 @@ export const ParentIssuesListModal: React.FC = ({
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 107c1f5281c..71d935d3c80 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -76,7 +76,7 @@ export const ProfileSidebar = observer(() => { return (
{userProjectsData ? ( @@ -162,12 +162,13 @@ export const ProfileSidebar = observer(() => { {project.assigned_issues > 0 && (
{completedIssuePercentage}%
diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index 08dfd55098e..5bf5f0eeae1 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -21,7 +21,9 @@ export const ProfileSettingsLayout: FC = (props) => {
{header} -
{children}
+
+ {children} +
diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 0e1d3158764..116813958f2 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -73,7 +73,9 @@ export const ProfilePreferenceSettingsLayout: FC
{header} -
{children}
+
+ {children} +
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 85f82961fee..3e515cc6472 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -129,7 +129,7 @@ export const ProfileLayoutSidebar = observer(() => { {!sidebarCollapsed && (
Your account
)} -
+
{PROFILE_ACTION_LINKS.map((link) => { if (link.key === "change-password" && currentUser?.is_password_autoset) return null; @@ -157,7 +157,7 @@ export const ProfileLayoutSidebar = observer(() => {
Workspaces
)} {workspacesList && workspacesList.length > 0 && ( -
+
{workspacesList.map((workspace) => ( = observer((props)
-
{children}
+
+ {children} +
); }); diff --git a/web/layouts/settings-layout/workspace/layout.tsx b/web/layouts/settings-layout/workspace/layout.tsx index 4ee0f1e3352..3d5d057bebd 100644 --- a/web/layouts/settings-layout/workspace/layout.tsx +++ b/web/layouts/settings-layout/workspace/layout.tsx @@ -10,11 +10,11 @@ export const WorkspaceSettingLayout: FC = (props) => { const { children } = props; return ( -
+
-
+
{children}
diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index a4d1debe1c9..7d24a8b1117 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -45,7 +45,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 3aea45adbc0..70108f90a0b 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -26,7 +26,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 06246f1c201..5c9faae7cda 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -56,7 +56,7 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { return ( <> -
+

Integrations

diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 3bb1c8c04b6..d62ac1e6653 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -19,7 +19,7 @@ const LabelsSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 1f203ff04c5..35366cb0a20 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -71,7 +71,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { <> setIsCreateTokenModalOpen(false)} /> -
+
{tokens.length > 0 ? ( <>
diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 46c7e99cb7b..19f23913efe 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -70,7 +70,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { return ( <> -
+
{

Activity

{userActivity ? ( -
+
    {userActivity.results.map((activityItem: any) => { if (activityItem.field === "comment") { diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index bdde41d08f6..c4eab324a8e 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -163,7 +163,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { )} /> setDeactivateAccountModal(false)} /> -
    +
    diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index 34bd6fb0305..ddd23abdfcc 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -28,7 +28,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => { return ( <> -
    +
    From 53367a6bc4cb4fbd49ede6582b7274b41d0ca278 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 6 Mar 2024 14:18:41 +0530 Subject: [PATCH 007/214] [WEB-570] chore: toast refactor (#3836) * new toast setup * chore: new toast implementation. * chore: move toast component to ui package. * chore: replace `setToast` with `setPromiseToast` in required places for better UX. * chore: code cleanup. * chore: update theme. * fix: theme switching issue. * chore: remove toast from issue update operations. * chore: add promise toast for add/ remove issue to cycle/ module and remove local spinners. --------- Co-authored-by: rahulramesha --- .../tailwind-config-custom/tailwind.config.js | 25 +++ packages/ui/package.json | 1 + packages/ui/src/index.ts | 1 + .../ui/src/spinners/circular-bar-spinner.tsx | 35 +++ packages/ui/src/spinners/index.ts | 1 + packages/ui/src/toast/index.tsx | 206 +++++++++++++++++ .../account/deactivate-account-modal.tsx | 18 +- .../account/o-auth/o-auth-options.tsx | 13 +- .../account/sign-in-forms/email.tsx | 9 +- .../sign-in-forms/optional-set-password.tsx | 13 +- .../account/sign-in-forms/password.tsx | 13 +- .../account/sign-in-forms/unique-code.tsx | 17 +- .../account/sign-up-forms/email.tsx | 9 +- .../sign-up-forms/optional-set-password.tsx | 16 +- .../account/sign-up-forms/password.tsx | 10 +- .../account/sign-up-forms/unique-code.tsx | 17 +- .../custom-analytics/sidebar/sidebar.tsx | 13 +- .../api-token/delete-token-modal.tsx | 14 +- .../api-token/modal/create-token-modal.tsx | 12 +- web/components/api-token/modal/form.tsx | 10 +- .../modal/generated-token-details.tsx | 10 +- .../actions/issue-actions/actions-list.tsx | 14 +- .../command-palette/actions/theme-actions.tsx | 8 +- .../command-palette/command-palette.tsx | 15 +- .../core/modals/bulk-delete-issues-modal.tsx | 18 +- .../modals/existing-issues-list-modal.tsx | 14 +- .../core/modals/gpt-assistant-popover.tsx | 13 +- .../core/modals/user-image-upload-modal.tsx | 10 +- .../modals/workspace-image-upload-modal.tsx | 10 +- web/components/core/sidebar/links-list.tsx | 11 +- .../cycles/active-cycle-details.tsx | 40 ++-- web/components/cycles/cycles-board-card.tsx | 81 ++++--- web/components/cycles/cycles-list-item.tsx | 81 ++++--- web/components/cycles/delete-modal.tsx | 13 +- web/components/cycles/modal.tsx | 25 +-- web/components/cycles/sidebar.tsx | 21 +- .../cycles/transfer-issues-modal.tsx | 14 +- .../create-update-estimate-modal.tsx | 29 ++- .../estimates/delete-estimate-modal.tsx | 9 +- .../estimates/estimate-list-item.tsx | 9 +- web/components/estimates/estimates-list.tsx | 9 +- web/components/exporter/export-modal.tsx | 14 +- web/components/inbox/inbox-issue-actions.tsx | 13 +- .../inbox/modals/create-issue-modal.tsx | 20 +- .../inbox/modals/select-duplicate.tsx | 11 +- web/components/instance/ai-form.tsx | 9 +- web/components/instance/email-form.tsx | 9 +- web/components/instance/general-form.tsx | 9 +- .../instance/github-config-form.tsx | 13 +- .../instance/google-config-form.tsx | 13 +- web/components/instance/image-config-form.tsx | 9 +- .../instance/setup-form/sign-in-form.tsx | 10 +- web/components/instance/sidebar-dropdown.tsx | 9 +- .../integration/delete-import-modal.tsx | 10 +- web/components/integration/github/root.tsx | 10 +- .../integration/single-integration-card.tsx | 13 +- web/components/issues/archive-issue-modal.tsx | 9 +- web/components/issues/attachment/root.tsx | 38 ++-- web/components/issues/delete-issue-modal.tsx | 9 +- web/components/issues/description-form.tsx | 14 +- web/components/issues/description-input.tsx | 8 +- .../issues/issue-detail/cycle-select.tsx | 1 - .../issues/issue-detail/inbox/root.tsx | 26 +-- .../issue-detail/issue-activity/root.tsx | 30 +-- .../issue-detail/label/create-label.tsx | 8 +- .../issues/issue-detail/label/root.tsx | 24 +- .../issues/issue-detail/links/link-detail.tsx | 8 +- .../issues/issue-detail/links/root.tsx | 31 ++- .../issues/issue-detail/module-select.tsx | 1 - .../issue-detail/reactions/issue-comment.tsx | 21 +- .../issues/issue-detail/reactions/issue.tsx | 22 +- .../issues/issue-detail/relation-select.tsx | 9 +- web/components/issues/issue-detail/root.tsx | 161 ++++++-------- .../issues/issue-detail/sidebar.tsx | 20 +- .../issues/issue-detail/subscription.tsx | 12 +- .../calendar/base-calendar-root.tsx | 9 +- .../calendar/quick-add-issue-form.tsx | 70 +++--- .../issue-layouts/empty-states/cycle.tsx | 9 +- .../issue-layouts/empty-states/module.tsx | 9 +- .../gantt/quick-add-issue-form.tsx | 48 ++-- .../issue-layouts/kanban/base-kanban-root.tsx | 9 +- .../kanban/headers/group-by-card.tsx | 10 +- .../kanban/quick-add-issue-form.tsx | 63 +++--- .../list/headers/group-by-card.tsx | 10 +- .../list/quick-add-issue-form.tsx | 52 +++-- .../quick-action-dropdowns/all-issue.tsx | 12 +- .../quick-action-dropdowns/archived-issue.tsx | 10 +- .../quick-action-dropdowns/cycle-issue.tsx | 15 +- .../quick-action-dropdowns/module-issue.tsx | 14 +- .../quick-action-dropdowns/project-issue.tsx | 14 +- .../spreadsheet/quick-add-issue-form.tsx | 79 ++++--- .../issues/issue-modal/draft-issue-layout.tsx | 13 +- web/components/issues/issue-modal/form.tsx | 17 +- web/components/issues/issue-modal/modal.tsx | 22 +- .../issues/peek-overview/header.tsx | 19 +- web/components/issues/peek-overview/root.tsx | 210 +++++++++--------- web/components/issues/peek-overview/view.tsx | 5 +- web/components/issues/sub-issues/root.tsx | 47 ++-- web/components/issues/title-input.tsx | 2 +- web/components/labels/create-label-modal.tsx | 9 +- .../labels/create-update-label-inline.tsx | 13 +- web/components/labels/delete-label-modal.tsx | 10 +- .../modules/delete-module-modal.tsx | 13 +- web/components/modules/modal.tsx | 21 +- web/components/modules/module-card-item.tsx | 71 +++--- web/components/modules/module-list-item.tsx | 80 ++++--- web/components/modules/sidebar.tsx | 50 +++-- .../notifications/notification-card.tsx | 29 ++- .../select-snooze-till-modal.tsx | 10 +- web/components/onboarding/invite-members.tsx | 12 +- .../switch-delete-account-modal.tsx | 17 +- web/components/onboarding/workspace.tsx | 17 +- web/components/pages/delete-page-modal.tsx | 14 +- .../preferences/email-notification-form.tsx | 10 +- web/components/project/card.tsx | 39 ++-- .../project/create-project-modal.tsx | 25 +-- .../project/delete-project-modal.tsx | 13 +- web/components/project/form.tsx | 13 +- web/components/project/integration-card.tsx | 13 +- .../project/leave-project-modal.tsx | 21 +- web/components/project/member-list-item.tsx | 17 +- .../project-settings-member-defaults.tsx | 9 +- .../project/publish-project/modal.tsx | 17 +- .../project/send-project-invitation-modal.tsx | 9 +- .../project/settings/features-list.tsx | 10 +- web/components/project/sidebar-list-item.tsx | 45 ++-- web/components/project/sidebar-list.tsx | 13 +- web/components/states/create-state-modal.tsx | 13 +- .../states/create-update-state-inline.tsx | 29 ++- web/components/states/delete-state-modal.tsx | 13 +- web/components/toast-alert/index.tsx | 61 ----- web/components/views/delete-view-modal.tsx | 13 +- web/components/views/modal.tsx | 17 +- web/components/views/view-list-item.tsx | 9 +- .../web-hooks/create-webhook-modal.tsx | 13 +- .../web-hooks/delete-webhook-modal.tsx | 13 +- web/components/web-hooks/form/secret-key.tsx | 22 +- .../workspace/create-workspace-form.tsx | 17 +- .../workspace/delete-workspace-modal.tsx | 13 +- .../settings/invitations-list-item.tsx | 17 +- .../workspace/settings/members-list-item.tsx | 17 +- .../workspace/settings/workspace-details.tsx | 21 +- web/components/workspace/sidebar-dropdown.tsx | 10 +- .../workspace/views/delete-view-modal.tsx | 9 +- web/components/workspace/views/modal.tsx | 21 +- web/contexts/toast.context.tsx | 97 -------- web/helpers/theme.helper.ts | 3 + web/hooks/use-toast.tsx | 9 - web/hooks/use-user-notifications.tsx | 14 +- .../settings-layout/profile/sidebar.tsx | 9 +- web/lib/app-provider.tsx | 53 ++--- .../archived-issues/[archivedIssueId].tsx | 12 +- .../projects/[projectId]/pages/[pageId].tsx | 9 +- .../[projectId]/settings/automations.tsx | 10 +- .../[workspaceSlug]/settings/members.tsx | 13 +- .../settings/webhooks/[webhookId].tsx | 14 +- web/pages/_app.tsx | 6 +- web/pages/_error.tsx | 10 +- web/pages/accounts/forgot-password.tsx | 13 +- web/pages/accounts/reset-password.tsx | 9 +- web/pages/god-mode/authorization.tsx | 15 +- web/pages/invitations/index.tsx | 17 +- web/pages/profile/change-password.tsx | 37 ++- web/pages/profile/index.tsx | 58 ++--- web/pages/profile/preferences/theme.tsx | 21 +- web/styles/globals.css | 42 ++++ yarn.lock | 5 + 167 files changed, 1827 insertions(+), 1896 deletions(-) create mode 100644 packages/ui/src/spinners/circular-bar-spinner.tsx create mode 100644 packages/ui/src/toast/index.tsx delete mode 100644 web/components/toast-alert/index.tsx delete mode 100644 web/contexts/toast.context.tsx delete mode 100644 web/hooks/use-toast.tsx diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 3465b819671..5d767e84fc8 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -198,6 +198,31 @@ module.exports = { 300: convertToRGB("--color-onboarding-border-300"), }, }, + toast: { + text: { + success: convertToRGB("--color-toast-success-text"), + error: convertToRGB("--color-toast-error-text"), + warning: convertToRGB("--color-toast-warning-text"), + info: convertToRGB("--color-toast-info-text"), + loading: convertToRGB("--color-toast-loading-text"), + secondary: convertToRGB("--color-toast-secondary-text"), + tertiary: convertToRGB("--color-toast-tertiary-text"), + }, + background: { + success: convertToRGB("--color-toast-success-background"), + error: convertToRGB("--color-toast-error-background"), + warning: convertToRGB("--color-toast-warning-background"), + info: convertToRGB("--color-toast-info-background"), + loading: convertToRGB("--color-toast-loading-background"), + }, + border: { + success: convertToRGB("--color-toast-success-border"), + error: convertToRGB("--color-toast-error-border"), + warning: convertToRGB("--color-toast-warning-border"), + info: convertToRGB("--color-toast-info-border"), + loading: convertToRGB("--color-toast-loading-border"), + }, + }, }, keyframes: { leftToaster: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 756a0f2f1be..91a010a1eac 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,6 +26,7 @@ "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", + "sonner": "^1.4.2", "tailwind-merge": "^2.0.0" }, "devDependencies": { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b90b6993a77..218d375fa97 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -10,3 +10,4 @@ export * from "./spinners"; export * from "./tooltip"; export * from "./loader"; export * from "./control-link"; +export * from "./toast"; diff --git a/packages/ui/src/spinners/circular-bar-spinner.tsx b/packages/ui/src/spinners/circular-bar-spinner.tsx new file mode 100644 index 00000000000..3be8af43aad --- /dev/null +++ b/packages/ui/src/spinners/circular-bar-spinner.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +interface ICircularBarSpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const CircularBarSpinner: React.FC = ({ + height = "16px", + width = "16px", + className = "", +}) => ( +
    + + + + + + + + + + + + +
    +); diff --git a/packages/ui/src/spinners/index.ts b/packages/ui/src/spinners/index.ts index 76856817261..a871a9b77b8 100644 --- a/packages/ui/src/spinners/index.ts +++ b/packages/ui/src/spinners/index.ts @@ -1 +1,2 @@ export * from "./circular-spinner"; +export * from "./circular-bar-spinner"; diff --git a/packages/ui/src/toast/index.tsx b/packages/ui/src/toast/index.tsx new file mode 100644 index 00000000000..7553262756a --- /dev/null +++ b/packages/ui/src/toast/index.tsx @@ -0,0 +1,206 @@ +import * as React from "react"; +import { Toaster, toast } from "sonner"; +// icons +import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react"; +// spinner +import { CircularBarSpinner } from "../spinners"; +// helper +import { cn } from "../../helpers"; + +export enum TOAST_TYPE { + SUCCESS = "success", + ERROR = "error", + INFO = "info", + WARNING = "warning", + LOADING = "loading", +} + +type SetToastProps = + | { + type: TOAST_TYPE.LOADING; + title?: string; + } + | { + id?: string | number; + type: Exclude; + title: string; + message?: string; + }; + +type PromiseToastCallback = (data: ToastData) => string; + +type PromiseToastData = { + title: string; + message?: PromiseToastCallback; +}; + +type PromiseToastOptions = { + loading?: string; + success: PromiseToastData; + error: PromiseToastData; +}; + +type ToastContentProps = { + toastId: string | number; + icon?: React.ReactNode; + textColorClassName: string; + backgroundColorClassName: string; + borderColorClassName: string; +}; + +type ToastProps = { + theme: "light" | "dark" | "system"; +}; + +export const Toast = (props: ToastProps) => { + const { theme } = props; + return ; +}; + +export const setToast = (props: SetToastProps) => { + const renderToastContent = ({ + toastId, + icon, + textColorClassName, + backgroundColorClassName, + borderColorClassName, + }: ToastContentProps) => + props.type === TOAST_TYPE.LOADING ? ( +
    { + e.stopPropagation(); + e.preventDefault(); + }} + className={cn("w-[350px] h-[67.3px] rounded-lg border shadow-sm p-2", backgroundColorClassName, borderColorClassName)} + > +
    + {icon &&
    {icon}
    } +
    +
    {props.title ?? "Loading..."}
    +
    + toast.dismiss(toastId)} + /> +
    +
    +
    +
    + ) : ( +
    { + e.stopPropagation(); + e.preventDefault(); + }} + className={cn( + "relative flex flex-col w-[350px] rounded-lg border shadow-sm p-2", + backgroundColorClassName, + borderColorClassName + )} + > + toast.dismiss(toastId)} + /> +
    + {icon &&
    {icon}
    } +
    +
    {props.title}
    + {props.message &&
    {props.message}
    } +
    +
    +
    + ); + + switch (props.type) { + case TOAST_TYPE.SUCCESS: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-success", + backgroundColorClassName: "bg-toast-background-success", + borderColorClassName: "border-toast-border-success", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.ERROR: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-error", + backgroundColorClassName: "bg-toast-background-error", + borderColorClassName: "border-toast-border-error", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.WARNING: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-warning", + backgroundColorClassName: "bg-toast-background-warning", + borderColorClassName: "border-toast-border-warning", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.INFO: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + textColorClassName: "text-toast-text-info", + backgroundColorClassName: "bg-toast-background-info", + borderColorClassName: "border-toast-border-info", + }), + props.id ? { id: props.id } : {} + ); + + case TOAST_TYPE.LOADING: + return toast.custom((toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-loading", + backgroundColorClassName: "bg-toast-background-loading", + borderColorClassName: "border-toast-border-loading", + }) + ); + } +}; + +export const setPromiseToast = ( + promise: Promise, + options: PromiseToastOptions +): void => { + const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading }); + + promise + .then((data: ToastData) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + id: tId, + title: options.success.title, + message: options.success.message?.(data), + }); + }) + .catch((data: ToastData) => { + setToast({ + type: TOAST_TYPE.ERROR, + id: tId, + title: options.error.title, + message: options.error.message?.(data), + }); + }); +}; diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 701db6ad945..41d1fd7ca4e 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -7,9 +7,7 @@ import { mutate } from "swr"; // hooks import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -26,7 +24,6 @@ export const DeactivateAccountModal: React.FC = (props) => { const router = useRouter(); - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); const handleClose = () => { @@ -39,8 +36,8 @@ export const DeactivateAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deactivated successfully.", }); @@ -50,8 +47,8 @@ export const DeactivateAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) @@ -90,7 +87,10 @@ export const DeactivateAccountModal: React.FC = (props) => {
    -
    diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index 7c8468acb78..bbb73b855d0 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { AuthService } from "services/auth.service"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { GitHubSignInButton, GoogleSignInButton } from "components/account"; @@ -17,8 +18,6 @@ const authService = new AuthService(); export const OAuthOptions: React.FC = observer((props) => { const { handleSignInRedirection, type } = props; - // toast alert - const { setToastAlert } = useToast(); // mobx store const { config: { envConfig }, @@ -39,9 +38,9 @@ export const OAuthOptions: React.FC = observer((props) => { if (response) handleSignInRedirection(); } else throw Error("Cant find credentials"); } catch (err: any) { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Error signing in!", - type: "error", message: err?.error || "Something went wrong. Please try again later or contact the support team.", }); } @@ -60,9 +59,9 @@ export const OAuthOptions: React.FC = observer((props) => { if (response) handleSignInRedirection(); } else throw Error("Cant find credentials"); } catch (err: any) { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Error signing in!", - type: "error", message: err?.error || "Something went wrong. Please try again later or contact the support team.", }); } diff --git a/web/components/account/sign-in-forms/email.tsx b/web/components/account/sign-in-forms/email.tsx index 67ef720fe63..881c75f8338 100644 --- a/web/components/account/sign-in-forms/email.tsx +++ b/web/components/account/sign-in-forms/email.tsx @@ -4,10 +4,8 @@ import { XCircle } from "lucide-react"; import { observer } from "mobx-react-lite"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -27,7 +25,6 @@ const authService = new AuthService(); export const SignInEmailForm: React.FC = observer((props) => { const { onSubmit, updateEmail } = props; // hooks - const { setToastAlert } = useToast(); const { control, formState: { errors, isSubmitting, isValid }, @@ -52,8 +49,8 @@ export const SignInEmailForm: React.FC = observer((props) => { .emailCheck(payload) .then((res) => onSubmit(res.is_password_autoset)) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index 1ea5ca79217..8fc7935cdec 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -3,10 +3,9 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // icons @@ -38,8 +37,6 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -62,8 +59,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { await authService .setPassword(payload) .then(async () => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Password created successfully.", }); @@ -78,8 +75,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { state: "FAILED", first_time: false, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index 98719df63a1..7d51b0cf557 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -6,12 +6,11 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useApplication, useEventTracker } from "hooks/store"; // components import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -43,8 +42,6 @@ export const SignInPasswordForm: React.FC = observer((props) => { // states const [isSendingUniqueCode, setIsSendingUniqueCode] = useState(false); const [showPassword, setShowPassword] = useState(false); - // toast alert - const { setToastAlert } = useToast(); const { config: { envConfig }, } = useApplication(); @@ -83,8 +80,8 @@ export const SignInPasswordForm: React.FC = observer((props) => { await onSubmit(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) @@ -107,8 +104,8 @@ export const SignInPasswordForm: React.FC = observer((props) => { .generateUniqueCode({ email: emailFormValue }) .then(() => handleStepChange(ESignInSteps.USE_UNIQUE_CODE_FROM_PASSWORD)) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 55dbe86e246..25ee4c462b2 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -5,11 +5,10 @@ import { XCircle } from "lucide-react"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -42,8 +41,6 @@ export const SignInUniqueCodeForm: React.FC = (props) => { const { email, onSubmit, handleEmailClear, submitButtonText } = props; // states const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureEvent } = useEventTracker(); // timer @@ -84,8 +81,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { captureEvent(CODE_VERIFIED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -101,8 +98,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { .generateUniqueCode(payload) .then(() => { setResendCodeTimer(30); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "A new unique code has been sent to your email.", }); @@ -113,8 +110,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/email.tsx b/web/components/account/sign-up-forms/email.tsx index 0d5861b4ee2..b65ca95bf59 100644 --- a/web/components/account/sign-up-forms/email.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -4,10 +4,8 @@ import { XCircle } from "lucide-react"; import { observer } from "mobx-react-lite"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -27,7 +25,6 @@ const authService = new AuthService(); export const SignUpEmailForm: React.FC = observer((props) => { const { onSubmit, updateEmail } = props; // hooks - const { setToastAlert } = useToast(); const { control, formState: { errors, isSubmitting, isValid }, @@ -52,8 +49,8 @@ export const SignUpEmailForm: React.FC = observer((props) => { .emailCheck(payload) .then(() => onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index b49adabbb5c..651f2815ffd 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -3,14 +3,14 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; -// constants +// components import { ESignUpSteps } from "components/account"; +// constants import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons import { Eye, EyeOff } from "lucide-react"; @@ -41,8 +41,6 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -65,8 +63,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { await authService .setPassword(payload) .then(async () => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Password created successfully.", }); @@ -81,8 +79,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { state: "FAILED", first_time: true, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/account/sign-up-forms/password.tsx b/web/components/account/sign-up-forms/password.tsx index 293e03ef874..5207a50243b 100644 --- a/web/components/account/sign-up-forms/password.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -5,10 +5,8 @@ import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -34,8 +32,6 @@ export const SignUpPasswordForm: React.FC = observer((props) => { const { onSubmit } = props; // states const [showPassword, setShowPassword] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -59,8 +55,8 @@ export const SignUpPasswordForm: React.FC = observer((props) => { .passwordSignIn(payload) .then(async () => await onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 1b54ef9ebc1..51705ea6782 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -6,11 +6,10 @@ import { XCircle } from "lucide-react"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types @@ -44,8 +43,6 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); // form info @@ -84,8 +81,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { captureEvent(CODE_VERIFIED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -101,8 +98,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { .generateUniqueCode(payload) .then(() => { setResendCodeTimer(30); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "A new unique code has been sent to your email.", }); @@ -112,8 +109,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index bf1c80fea79..aab7f874f28 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -6,11 +6,10 @@ import { mutate } from "swr"; import { AnalyticsService } from "services/analytics.service"; // hooks import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CalendarDays, Download, RefreshCw } from "lucide-react"; // helpers @@ -34,8 +33,6 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser } = useUser(); const { workspaceProjectIds, getProjectById } = useProject(); @@ -107,8 +104,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { analyticsService .exportAnalytics(workspaceSlug.toString(), data) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: res.message, }); @@ -116,8 +113,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { trackExportAnalytics(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in exporting the analytics. Please try again.", }) diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index 993289c10c7..472431df3e4 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -4,10 +4,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { APITokenService } from "services/api_token.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IApiToken } from "@plane/types"; // fetch-keys @@ -25,8 +23,6 @@ export const DeleteApiTokenModal: FC = (props) => { const { isOpen, onClose, tokenId } = props; // states const [deleteLoading, setDeleteLoading] = useState(false); - // hooks - const { setToastAlert } = useToast(); // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -44,8 +40,8 @@ export const DeleteApiTokenModal: FC = (props) => { apiTokenService .deleteApiToken(workspaceSlug.toString(), tokenId) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Token deleted successfully.", }); @@ -59,8 +55,8 @@ export const DeleteApiTokenModal: FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.message ?? "Something went wrong. Please try again.", }) diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index b3fc3df78ec..c90e743bc6c 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -4,8 +4,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { APITokenService } from "services/api_token.service"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateApiTokenForm, GeneratedTokenDetails } from "components/api-token"; // helpers @@ -32,8 +32,6 @@ export const CreateApiTokenModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -76,10 +74,10 @@ export const CreateApiTokenModal: React.FC = (props) => { ); }) .catch((err) => { - setToastAlert({ - message: err.message, - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", + message: err.message, }); throw err; diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index 77753e64d20..9fc16081593 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -3,10 +3,8 @@ import { add } from "date-fns"; import { Controller, useForm } from "react-hook-form"; import { DateDropdown } from "components/dropdowns"; import { Calendar } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types @@ -66,8 +64,6 @@ export const CreateApiTokenForm: React.FC = (props) => { const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props; // states const [customDate, setCustomDate] = useState(null); - // toast alert - const { setToastAlert } = useToast(); // form const { control, @@ -80,8 +76,8 @@ export const CreateApiTokenForm: React.FC = (props) => { const handleFormSubmit = async (data: IApiToken) => { // if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error if (!neverExpires && (!data.expired_at || (data.expired_at === "custom" && !customDate))) - return setToastAlert({ - type: "error", + return setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select an expiration date.", }); diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index f28ea348126..fcae6b2496d 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -1,8 +1,6 @@ import { Copy } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Tooltip } from "@plane/ui"; +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -17,12 +15,10 @@ type Props = { export const GeneratedTokenDetails: React.FC = (props) => { const { handleClose, tokenDetails } = props; - const { setToastAlert } = useToast(); - const copyApiToken = (token: string) => { copyTextToClipboard(token).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Token copied to clipboard.", }) diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 55f72c85d11..75bacb0c33d 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -4,10 +4,8 @@ import { Command } from "cmdk"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; // hooks import { useApplication, useUser, useIssues } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +import { DoubleCircleIcon, UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -37,8 +35,6 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { } = useApplication(); const { currentUser } = useUser(); - const { setToastAlert } = useToast(); - const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueDetails) return; @@ -71,14 +67,14 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index 976a63c871d..187bdfec899 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -5,7 +5,8 @@ import { Settings } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // constants import { THEME_OPTIONS } from "constants/themes"; @@ -21,15 +22,14 @@ export const CommandPaletteThemeActions: FC = observer((props) => { const { updateCurrentUserTheme } = useUser(); // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); const updateUserTheme = async (newTheme: string) => { setTheme(newTheme); return updateCurrentUserTheme(newTheme).catch(() => { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Failed to save user theme settings!", - type: "error", }); }); }; diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 396003589a4..e878489f40e 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -4,7 +4,8 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CommandModal, ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; @@ -63,8 +64,6 @@ export const CommandPalette: FC = observer(() => { createIssueStoreType, } = commandPalette; - const { setToastAlert } = useToast(); - const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -78,18 +77,18 @@ export const CommandPalette: FC = observer(() => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); - }, [setToastAlert, issueId]); + }, [issueId]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 39be2872b59..7eeb791745f 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -6,10 +6,8 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // services import { IssueService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Search } from "lucide-react"; // types @@ -55,8 +53,6 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { : null ); - const { setToastAlert } = useToast(); - const { handleSubmit, watch, @@ -79,8 +75,8 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { if (!workspaceSlug || !projectId) return; if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -91,16 +87,16 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { await removeBulkIssues(workspaceSlug as string, projectId as string, data.delete_issue_ids) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issues deleted successfully!", }); handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 1b6a1e76bdc..a1f8bfaa4ac 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -3,11 +3,9 @@ import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Rocket, Search, X } from "lucide-react"; // services import { ProjectService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; import useDebounce from "hooks/use-debounce"; // ui -import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; @@ -43,8 +41,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { const debouncedSearchTerm: string = useDebounce(searchTerm, 500); - const { setToastAlert } = useToast(); - const handleClose = () => { onClose(); setSearchTerm(""); @@ -54,8 +50,8 @@ export const ExistingIssuesListModal: React.FC = (props) => { const onSubmit = async () => { if (selectedIssues.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -69,9 +65,9 @@ export const ExistingIssuesListModal: React.FC = (props) => { handleClose(); - setToastAlert({ + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", - type: "success", message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, }); }; diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 590015e122c..49c2f4326ff 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -3,10 +3,9 @@ import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services import { AIService } from "services/ai.service"; // hooks -import useToast from "hooks/use-toast"; import { usePopper } from "react-popper"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor"; import { Popover, Transition } from "@headlessui/react"; @@ -44,8 +43,6 @@ export const GptAssistantPopover: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", @@ -78,8 +75,8 @@ export const GptAssistantPopover: React.FC = (props) => { ? error || "You have reached the maximum number of requests of 50 requests per month per user." : error || "Some error occurred. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorMessage, }); @@ -104,8 +101,8 @@ export const GptAssistantPopover: React.FC = (props) => { }; const handleInvalidTask = () => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter some task to get AI assistance.", }); diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 6debc2c15e2..3d0fbb9ee11 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -6,10 +6,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { UserCircle2 } from "lucide-react"; // constants @@ -32,8 +30,6 @@ export const UserImageUploadModal: React.FC = observer((props) => { // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { config: { envConfig }, @@ -76,8 +72,8 @@ export const UserImageUploadModal: React.FC = observer((props) => { if (value) fileService.deleteUserFile(value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index e04ccf8209d..eec62b91909 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -7,10 +7,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { UserCircle2 } from "lucide-react"; // constants @@ -37,8 +35,6 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { config: { envConfig }, } = useApplication(); @@ -83,8 +79,8 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 48a5e16b721..6b987e30833 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -1,5 +1,5 @@ // ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; // helpers @@ -7,7 +7,6 @@ import { calculateTimeAgo } from "helpers/date-time.helper"; // types import { ILinkDetails, UserAuth } from "@plane/types"; // hooks -import useToast from "hooks/use-toast"; import { observer } from "mobx-react"; import { useMeasure } from "@nivo/core"; import { useMember } from "hooks/store"; @@ -20,18 +19,16 @@ type Props = { }; export const LinksList: React.FC = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => { - // toast - const { setToastAlert } = useToast(); const { getUserDetails } = useMember(); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); - setToastAlert({ - message: "The URL has been successfully copied to your clipboard", - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", + message: "The URL has been successfully copied to your clipboard", }); }; diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 425ce7df3fb..bc22cb8ab12 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -5,7 +5,6 @@ import useSWR from "swr"; import { useTheme } from "next-themes"; // hooks import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; import { @@ -18,6 +17,7 @@ import { PriorityIcon, Avatar, CycleGroupIcon, + setPromiseToast, } from "@plane/ui"; // components import ProgressChart from "components/core/sidebar/progress-chart"; @@ -60,8 +60,6 @@ export const ActiveCycleDetails: React.FC = observer((props } = useCycle(); const { currentProjectDetails } = useProject(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const { isLoading } = useSWR( workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, @@ -119,12 +117,18 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, }); }; @@ -132,12 +136,22 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + activeCycle.id + ); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, }); }; diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 7d6b1e00036..72af8409df3 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -4,11 +4,20 @@ import Link from "next/link"; import { observer } from "mobx-react"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui -import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui"; +import { + Avatar, + AvatarGroup, + CustomMenu, + Tooltip, + LayersIcon, + CycleGroupIcon, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // icons import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers @@ -41,8 +50,6 @@ export const CyclesBoardCard: FC = observer((props) => { } = useUser(); const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // computed const cycleDetails = getCycleById(cycleId); @@ -81,8 +88,8 @@ export const CyclesBoardCard: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -93,42 +100,56 @@ export const CyclesBoardCard: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 31958cd847f..9ab2e3de8ed 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -4,11 +4,20 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui -import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar } from "@plane/ui"; +import { + CustomMenu, + Tooltip, + CircularProgressIndicator, + CycleGroupIcon, + AvatarGroup, + Avatar, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // icons import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // helpers @@ -45,8 +54,6 @@ export const CyclesListItem: FC = observer((props) => { } = useUser(); const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); @@ -54,8 +61,8 @@ export const CyclesListItem: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -66,42 +73,56 @@ export const CyclesListItem: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "List layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "List layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 5dc0306ab45..239fe6a663b 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useCycle } from "hooks/store"; -import useToast from "hooks/use-toast"; // components -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ICycle } from "@plane/types"; // constants @@ -31,8 +30,6 @@ export const CycleDeleteModal: React.FC = observer((props) => { // store hooks const { captureCycleEvent } = useEventTracker(); const { deleteCycle } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const formSubmit = async () => { if (!cycle) return; @@ -41,8 +38,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { try { await deleteCycle(workspaceSlug, projectId, cycle.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle deleted successfully.", }); @@ -62,8 +59,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { handleClose(); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Warning!", message: "Something went wrong please try again later.", }); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index b22afb2b44d..1d60f1dc4fd 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -4,10 +4,11 @@ import { Dialog, Transition } from "@headlessui/react"; import { CycleService } from "services/cycle.service"; // hooks import { useEventTracker, useCycle, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components import { CycleForm } from "components/cycles"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; // constants @@ -32,8 +33,6 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const { captureCycleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createCycle, updateCycleDetails } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); @@ -43,8 +42,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const selectedProjectId = payload.project_id ?? projectId.toString(); await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle created successfully.", }); @@ -54,8 +53,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in creating cycle. Please try again.", }); @@ -77,8 +76,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); @@ -88,8 +87,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...payload, state: "FAILED" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in updating cycle. Please try again.", }); @@ -138,8 +137,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } handleClose(); } else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.", }); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 646736bd2b6..f01c840f1fc 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -8,13 +8,12 @@ import isEmpty from "lodash/isEmpty"; import { CycleService } from "services/cycle.service"; // hooks import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; // ui -import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; +import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; // helpers @@ -60,8 +59,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { // derived values const cycleDetails = getCycleById(cycleId); const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, reset } = useForm({ defaultValues, @@ -98,15 +95,15 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); @@ -147,14 +144,14 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { if (isDateValid) { submitChanges(payload, "date_range"); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.", diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index adff1954578..be3b26a7b72 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -3,10 +3,10 @@ import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useCycle, useIssues } from "hooks/store"; +// ui +import { ContrastIcon, TransferIcon, TOAST_TYPE, setToast } from "@plane/ui"; //icons -import { ContrastIcon, TransferIcon } from "@plane/ui"; import { AlertCircle, Search, X } from "lucide-react"; // constants import { EIssuesStoreType } from "constants/issue"; @@ -30,23 +30,21 @@ export const TransferIssuesModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { setToastAlert } = useToast(); - const transferIssue = async (payload: any) => { if (!workspaceSlug || !projectId || !cycleId) return; // TODO: import transferIssuesFromCycle from store await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issues transferred successfully", message: "Issues have been transferred successfully", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issues cannot be transfer. Please try again.", }); diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 0a607e88db1..1ca39c84abf 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // store hooks import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input, TextArea } from "@plane/ui"; +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types @@ -40,8 +39,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { // store hooks const { createEstimate, updateEstimate } = useEstimate(); // form info - // toast alert - const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -67,8 +64,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? err.status === 400 @@ -89,8 +86,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be updated. Please try again.", }); @@ -99,8 +96,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const onSubmit = async (formData: FormValues) => { if (!formData.name || formData.name === "") { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate title cannot be empty.", }); @@ -115,8 +112,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5 === "" || formData.value6 === "" ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot be empty.", }); @@ -131,8 +128,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5.length > 20 || formData.value6.length > 20 ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot have more than 20 characters.", }); @@ -149,8 +146,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value6, ]) ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate points cannot have duplicate values.", }); diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index 8055ddb90d0..ac51d231273 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; // types import { IEstimate } from "@plane/types"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -26,8 +25,6 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteEstimate } = useEstimate(); - // toast alert - const { setToastAlert } = useToast(); const handleEstimateDelete = () => { if (!workspaceSlug || !projectId) return; @@ -43,8 +40,8 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be deleted. Please try again", }); diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index b6effa71183..37932a0accf 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -3,9 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomMenu } from "@plane/ui"; +import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; //icons import { Pencil, Trash2 } from "lucide-react"; // helpers @@ -26,8 +25,6 @@ export const EstimateListItem: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { currentProjectDetails, updateProject } = useProject(); - // hooks - const { setToastAlert } = useToast(); const handleUseEstimate = async () => { if (!workspaceSlug || !projectId) return; @@ -38,8 +35,8 @@ export const EstimateListItem: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate points could not be used. Please try again.", }); diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 1dabc618148..711f713a670 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -4,12 +4,11 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // store hooks import { useEstimate, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers @@ -31,8 +30,6 @@ export const EstimatesList: React.FC = observer(() => { const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); const { currentUser } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -50,8 +47,8 @@ export const EstimatesList: React.FC = observer(() => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be disabled. Please try again", }); diff --git a/web/components/exporter/export-modal.tsx b/web/components/exporter/export-modal.tsx index b1f5297754c..f38550b3a28 100644 --- a/web/components/exporter/export-modal.tsx +++ b/web/components/exporter/export-modal.tsx @@ -6,10 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { useProject } from "hooks/store"; // services import { ProjectExportService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSearchSelect } from "@plane/ui"; +import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IImporterService } from "@plane/types"; @@ -34,8 +32,6 @@ export const Exporter: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { workspaceProjectIds, getProjectById } = useProject(); - // toast alert - const { setToastAlert } = useToast(); const options = workspaceProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); @@ -71,8 +67,8 @@ export const Exporter: React.FC = observer((props) => { mutateServices(); router.push(`/${workspaceSlug}/settings/exports`); setExportLoading(false); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Export Successful", message: `You will be able to download the exported ${ provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : "" @@ -81,8 +77,8 @@ export const Exporter: React.FC = observer((props) => { }) .catch(() => { setExportLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Export was unsuccessful. Please try again.", }); diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 48d9157c63e..200d541abae 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -5,7 +5,6 @@ import { DayPicker } from "react-day-picker"; import { Popover } from "@headlessui/react"; // hooks import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { AcceptIssueModal, @@ -14,7 +13,7 @@ import { SelectDuplicateInboxIssueModal, } from "components/inbox"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; // types @@ -51,7 +50,6 @@ export const InboxIssueActionsHeader: FC = observer((p currentUser, membership: { currentProjectRole }, } = useUser(); - const { setToastAlert } = useToast(); // states const [date, setDate] = useState(new Date()); @@ -74,8 +72,8 @@ export const InboxIssueActionsHeader: FC = observer((p if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) throw new Error("Missing required parameters"); await updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while updating inbox status. Please try again.", }); @@ -98,8 +96,8 @@ export const InboxIssueActionsHeader: FC = observer((p pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting inbox issue. Please try again.", }); @@ -122,7 +120,6 @@ export const InboxIssueActionsHeader: FC = observer((p inboxIssueId, updateInboxIssueStatus, removeInboxIssue, - setToastAlert, captureIssueEvent, router, ] diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 84c4bef1ea8..2ef65c49762 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -7,7 +7,6 @@ import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { Sparkle } from "lucide-react"; // hooks import { useApplication, useEventTracker, useWorkspace, useInboxIssues, useMention } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { FileService } from "services/file.service"; import { AIService } from "services/ai.service"; @@ -15,7 +14,7 @@ import { AIService } from "services/ai.service"; import { PriorityDropdown } from "components/dropdowns"; import { GptAssistantPopover } from "components/core"; // ui -import { Button, Input, ToggleSwitch } from "@plane/ui"; +import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -46,9 +45,6 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); // refs const editorRef = useRef(null); - // toast alert - const { setToastAlert } = useToast(); - const { mentionHighlights, mentionSuggestions } = useMention(); // router const router = useRouter(); const { workspaceSlug, projectId, inboxId } = router.query as { @@ -56,6 +52,8 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { projectId: string; inboxId: string; }; + // hooks + const { mentionHighlights, mentionSuggestions } = useMention(); const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; @@ -138,8 +136,8 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { }) .then((res) => { if (res.response === "") - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue title isn't informative enough to generate the description. Please try with a different title.", @@ -150,14 +148,14 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const error = err?.data?.error; if (err.status === 429) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "You have reached the maximum number of requests of 50 requests per month per user.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "Some error occurred. Please try again.", }); diff --git a/web/components/inbox/modals/select-duplicate.tsx b/web/components/inbox/modals/select-duplicate.tsx index e4acca626e2..c9e0a1dadb2 100644 --- a/web/components/inbox/modals/select-duplicate.tsx +++ b/web/components/inbox/modals/select-duplicate.tsx @@ -2,13 +2,10 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; - -// hooks -import useToast from "hooks/use-toast"; // services import { IssueService } from "services/issue"; // ui -import { Button, LayersIcon } from "@plane/ui"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Search } from "lucide-react"; // fetch-keys @@ -30,8 +27,6 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const [query, setQuery] = useState(""); const [selectedItem, setSelectedItem] = useState(""); - const { setToastAlert } = useToast(); - const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -62,9 +57,9 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const handleSubmit = () => { if (!selectedItem || selectedItem.length === 0) - return setToastAlert({ + return setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, }); onSubmit(selectedItem); handleClose(); diff --git a/web/components/instance/ai-form.tsx b/web/components/instance/ai-form.tsx index 50ea90096bc..250feb511c6 100644 --- a/web/components/instance/ai-form.tsx +++ b/web/components/instance/ai-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceAIForm { config: IFormattedInstanceConfiguration; @@ -24,8 +23,6 @@ export const InstanceAIForm: FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -44,9 +41,9 @@ export const InstanceAIForm: FC = (props) => { await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "AI Settings updated successfully", }) ) diff --git a/web/components/instance/email-form.tsx b/web/components/instance/email-form.tsx index 9a97f32883d..9e9e4a86550 100644 --- a/web/components/instance/email-form.tsx +++ b/web/components/instance/email-form.tsx @@ -1,13 +1,12 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Input, ToggleSwitch } from "@plane/ui"; +import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { Eye, EyeOff } from "lucide-react"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceEmailForm { config: IFormattedInstanceConfiguration; @@ -29,8 +28,6 @@ export const InstanceEmailForm: FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -55,9 +52,9 @@ export const InstanceEmailForm: FC = (props) => { await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Email Settings updated successfully", }) ) diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx index 7fa06265f9d..6fedc88313e 100644 --- a/web/components/instance/general-form.tsx +++ b/web/components/instance/general-form.tsx @@ -1,12 +1,11 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IInstance, IInstanceAdmin } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGeneralForm { instance: IInstance; @@ -22,8 +21,6 @@ export const InstanceGeneralForm: FC = (props) => { const { instance, instanceAdmins } = props; // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceGeneralForm: FC = (props) => { await instanceStore .updateInstanceInfo(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Settings updated successfully", }) ) diff --git a/web/components/instance/github-config-form.tsx b/web/components/instance/github-config-form.tsx index 75639f82bd3..20fb08aff58 100644 --- a/web/components/instance/github-config-form.tsx +++ b/web/components/instance/github-config-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Copy, Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGithubConfigForm { config: IFormattedInstanceConfiguration; @@ -24,8 +23,6 @@ export const InstanceGithubConfigForm: FC = (props) = const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -44,9 +41,9 @@ export const InstanceGithubConfigForm: FC = (props) = await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Github Configuration Settings updated successfully", }) ) @@ -145,9 +142,9 @@ export const InstanceGithubConfigForm: FC = (props) = className="flex items-center justify-between py-2" onClick={() => { navigator.clipboard.writeText(originURL); - setToastAlert({ + setToast({ message: "The Origin URL has been successfully copied to your clipboard", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }} diff --git a/web/components/instance/google-config-form.tsx b/web/components/instance/google-config-form.tsx index cd7c3ab7d60..27d4f43007b 100644 --- a/web/components/instance/google-config-form.tsx +++ b/web/components/instance/google-config-form.tsx @@ -2,12 +2,11 @@ import { FC } from "react"; import { Controller, useForm } from "react-hook-form"; import { Copy } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceGoogleConfigForm { config: IFormattedInstanceConfiguration; @@ -22,8 +21,6 @@ export const InstanceGoogleConfigForm: FC = (props) = const { config } = props; // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceGoogleConfigForm: FC = (props) = await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Google Configuration Settings updated successfully", }) ) @@ -94,9 +91,9 @@ export const InstanceGoogleConfigForm: FC = (props) = className="flex items-center justify-between py-2" onClick={() => { navigator.clipboard.writeText(originURL); - setToastAlert({ + setToast({ message: "The Origin URL has been successfully copied to your clipboard", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }} diff --git a/web/components/instance/image-config-form.tsx b/web/components/instance/image-config-form.tsx index 93ce8871937..26694c4ecdb 100644 --- a/web/components/instance/image-config-form.tsx +++ b/web/components/instance/image-config-form.tsx @@ -2,12 +2,11 @@ import { FC, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; -import useToast from "hooks/use-toast"; export interface IInstanceImageConfigForm { config: IFormattedInstanceConfiguration; @@ -23,8 +22,6 @@ export const InstanceImageConfigForm: FC = (props) => const [showPassword, setShowPassword] = useState(false); // store hooks const { instance: instanceStore } = useApplication(); - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -42,9 +39,9 @@ export const InstanceImageConfigForm: FC = (props) => await instanceStore .updateInstanceConfigurations(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Image Configuration Settings updated successfully", }) ) diff --git a/web/components/instance/setup-form/sign-in-form.tsx b/web/components/instance/setup-form/sign-in-form.tsx index c4a9de6a396..174c66e825f 100644 --- a/web/components/instance/setup-form/sign-in-form.tsx +++ b/web/components/instance/setup-form/sign-in-form.tsx @@ -4,12 +4,10 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; // hooks import { useUser } from "hooks/store"; // ui -import { Input, Button } from "@plane/ui"; +import { Input, Button, TOAST_TYPE, setToast } from "@plane/ui"; // services import { AuthService } from "services/auth.service"; const authService = new AuthService(); -// hooks -import useToast from "hooks/use-toast"; // helpers import { checkEmailValidity } from "helpers/string.helper"; @@ -40,8 +38,6 @@ export const InstanceSetupSignInForm: FC = (props) => { password: "", }, }); - // hooks - const { setToastAlert } = useToast(); const handleFormSubmit = async (formValues: InstanceSetupEmailFormValues) => { const payload = { @@ -56,8 +52,8 @@ export const InstanceSetupSignInForm: FC = (props) => { handleNextStep(formValues.email); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 2bac426d62a..e763663d2eb 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -10,10 +10,8 @@ import { Menu, Transition } from "@headlessui/react"; import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react"; // hooks import { useApplication, useUser } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Avatar, Tooltip } from "@plane/ui"; +import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // Static Data const PROFILE_LINKS = [ @@ -35,7 +33,6 @@ export const InstanceSidebarDropdown = observer(() => { } = useApplication(); const { signOut, currentUser, currentUserSettings } = useUser(); // hooks - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); // redirect url for normal mode @@ -53,8 +50,8 @@ export const InstanceSidebarDropdown = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/components/integration/delete-import-modal.tsx b/web/components/integration/delete-import-modal.tsx index bc1351125e0..ee9fadaa0a6 100644 --- a/web/components/integration/delete-import-modal.tsx +++ b/web/components/integration/delete-import-modal.tsx @@ -8,10 +8,8 @@ import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; // services import { IntegrationService } from "services/integrations/integration.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { AlertTriangle } from "lucide-react"; // types @@ -36,8 +34,6 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const handleDeletion = () => { if (!workspaceSlug || !data) return; @@ -52,8 +48,8 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } integrationService .deleteImporterService(workspaceSlug as string, data.service, data.id) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) diff --git a/web/components/integration/github/root.tsx b/web/components/integration/github/root.tsx index bc577328aa6..f26f651fe71 100644 --- a/web/components/integration/github/root.tsx +++ b/web/components/integration/github/root.tsx @@ -10,8 +10,6 @@ import useSWR, { mutate } from "swr"; import { useForm } from "react-hook-form"; // services import { IntegrationService, GithubIntegrationService } from "services/integrations"; -// hooks -import useToast from "hooks/use-toast"; // components import { GithubImportConfigure, @@ -21,7 +19,7 @@ import { GithubImportConfirm, } from "components/integration"; // icons -import { UserGroupIcon } from "@plane/ui"; +import { UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react"; // images import GithubLogo from "public/services/github.png"; @@ -92,8 +90,6 @@ export const GithubImporterRoot: React.FC = () => { const router = useRouter(); const { workspaceSlug, provider } = router.query; - const { setToastAlert } = useToast(); - const { handleSubmit, control, setValue, watch } = useForm({ defaultValues: defaultFormValues, }); @@ -149,8 +145,8 @@ export const GithubImporterRoot: React.FC = () => { mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Import was unsuccessful. Please try again.", }) diff --git a/web/components/integration/single-integration-card.tsx b/web/components/integration/single-integration-card.tsx index 3026d698114..b36082b67c4 100644 --- a/web/components/integration/single-integration-card.tsx +++ b/web/components/integration/single-integration-card.tsx @@ -8,10 +8,9 @@ import useSWR, { mutate } from "swr"; import { IntegrationService } from "services/integrations"; // hooks import { useApplication, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; import useIntegrationPopup from "hooks/use-integration-popup"; // ui -import { Button, Loader, Tooltip } from "@plane/ui"; +import { Button, Loader, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import GithubLogo from "public/services/github.png"; import SlackLogo from "public/services/slack.png"; @@ -54,8 +53,6 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) const { membership: { currentWorkspaceRole }, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const isUserAdmin = currentWorkspaceRole === 20; @@ -87,8 +84,8 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) ); setDeletingIntegration(false); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Deleted successfully!", message: `${integration.title} integration deleted successfully.`, }); @@ -96,8 +93,8 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) .catch(() => { setDeletingIntegration(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: `${integration.title} integration could not be deleted. Please try again.`, }); diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx index 94c4c801a43..03207795761 100644 --- a/web/components/issues/archive-issue-modal.tsx +++ b/web/components/issues/archive-issue-modal.tsx @@ -3,9 +3,8 @@ import { Dialog, Transition } from "@headlessui/react"; // hooks import { useProject } from "hooks/store"; import { useIssues } from "hooks/store/use-issues"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; @@ -24,8 +23,6 @@ export const ArchiveIssueModal: React.FC = (props) => { // store hooks const { getProjectById } = useProject(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); if (!dataId && !data) return null; @@ -44,8 +41,8 @@ export const ArchiveIssueModal: React.FC = (props) => { await onSubmit() .then(() => onClose()) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }) diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index ffa17d3371e..fa3d0c22040 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -1,7 +1,8 @@ import { FC, useMemo } from "react"; // hooks import { useEventTracker, useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components import { IssueAttachmentUpload } from "./attachment-upload"; import { IssueAttachmentsList } from "./attachments-list"; @@ -24,19 +25,27 @@ export const IssueAttachmentRoot: FC = (props) => { // hooks const { createAttachment, removeAttachment } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const handleAttachmentOperations: TAttachmentOperations = useMemo( () => ({ create: async (data: FormData) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); - const res = await createAttachment(workspaceSlug, projectId, issueId, data); - setToastAlert({ - message: "The attachment has been successfully uploaded", - type: "success", - title: "Attachment uploaded", + + const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data); + setPromiseToast(attachmentUploadPromise, { + loading: "Uploading attachment...", + success: { + title: "Attachment uploaded", + message: () => "The attachment has been successfully uploaded", + }, + error: { + title: "Attachment not uploaded", + message: () => "The attachment could not be uploaded", + }, }); + + const res = await attachmentUploadPromise; captureIssueEvent({ eventName: "Issue attachment added", payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -50,20 +59,15 @@ export const IssueAttachmentRoot: FC = (props) => { eventName: "Issue attachment added", payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, }); - setToastAlert({ - message: "The attachment could not be uploaded", - type: "error", - title: "Attachment not uploaded", - }); } }, remove: async (attachmentId: string) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); - setToastAlert({ + setToast({ message: "The attachment has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Attachment removed", }); captureIssueEvent({ @@ -83,15 +87,15 @@ export const IssueAttachmentRoot: FC = (props) => { change_details: "", }, }); - setToastAlert({ + setToast({ message: "The Attachment could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Attachment not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert] + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment] ); return ( diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 3a9c0653edf..b8e6b3f8592 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -2,9 +2,7 @@ import { useEffect, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { useIssues } from "hooks/store/use-issues"; import { TIssue } from "@plane/types"; @@ -25,7 +23,6 @@ export const DeleteIssueModal: React.FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); - const { setToastAlert } = useToast(); // hooks const { getProjectById } = useProject(); @@ -50,9 +47,9 @@ export const DeleteIssueModal: React.FC = (props) => { onClose(); }) .catch(() => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to delete issue", }); }) diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index c64c147ea90..1ee22a6cb1a 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -71,16 +71,10 @@ export const IssueDescriptionForm: FC = observer((props) => { async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - await issueOperations.update( - workspaceSlug, - projectId, - issueId, - { - name: formData.name ?? "", - description_html: formData.description_html ?? "

    ", - }, - false - ); + await issueOperations.update(workspaceSlug, projectId, issueId, { + name: formData.name ?? "", + description_html: formData.description_html ?? "

    ", + }); }, [workspaceSlug, projectId, issueId, issueOperations] ); diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 79634fa84aa..65e82df5fc3 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -41,11 +41,9 @@ export const IssueDescriptionInput: FC = (props) => useEffect(() => { if (debouncedValue && debouncedValue !== value) { - issueOperations - .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) - .finally(() => { - setIsSubmitting("submitted"); - }); + issueOperations.update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }).finally(() => { + setIsSubmitting("submitted"); + }); } // DO NOT Add more dependencies here. It will cause multiple requests to be sent. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 84dccefac48..4da762a9dbc 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -56,7 +56,6 @@ export const IssueCycleSelect: React.FC = observer((props) => dropdownArrow dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" /> - {isUpdating && }
    ); }); diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index d96b36efae3..9b0e961c0bb 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -6,7 +6,8 @@ import { InboxIssueMainContent } from "./main-content"; import { InboxIssueDetailsSidebar } from "./sidebar"; // hooks import { useEventTracker, useInboxIssues, useIssueDetail, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; @@ -34,7 +35,6 @@ export const InboxIssueDetailRoot: FC = (props) => { fetchComments, } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); @@ -53,17 +53,9 @@ export const InboxIssueDetailRoot: FC = (props) => { projectId: string, issueId: string, data: Partial, - showToast: boolean = true ) => { try { await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); - if (showToast) { - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); - } captureIssueEvent({ eventName: "Inbox issue updated", payload: { ...data, state: "SUCCESS", element: "Inbox" }, @@ -74,9 +66,9 @@ export const InboxIssueDetailRoot: FC = (props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); captureIssueEvent({ @@ -93,9 +85,9 @@ export const InboxIssueDetailRoot: FC = (props) => { remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await removeInboxIssue(workspaceSlug, projectId, inboxId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -109,15 +101,15 @@ export const InboxIssueDetailRoot: FC = (props) => { payload: { id: issueId, state: "FAILED", element: "Inbox" }, path: router.asPath, }); - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); } }, }), - [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue, setToastAlert] + [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue] ); useSWR( diff --git a/web/components/issues/issue-detail/issue-activity/root.tsx b/web/components/issues/issue-detail/issue-activity/root.tsx index 695b248de20..673e6c2afc2 100644 --- a/web/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/root.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { History, LucideIcon, MessageCircle, ListRestart } from "lucide-react"; // hooks import { useIssueDetail, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueActivityCommentRoot, IssueActivityRoot, IssueCommentRoot, IssueCommentCreate } from "./"; // types @@ -45,7 +46,6 @@ export const IssueActivity: FC = observer((props) => { const { workspaceSlug, projectId, issueId } = props; // hooks const { createComment, updateComment, removeComment } = useIssueDetail(); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); // state const [activityTab, setActivityTab] = useState("all"); @@ -56,15 +56,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createComment(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ title: "Comment created successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment created successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment creation failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment creation failed. Please try again later.", }); } @@ -73,15 +73,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await updateComment(workspaceSlug, projectId, issueId, commentId, data); - setToastAlert({ + setToast({ title: "Comment updated successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment updated successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment update failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment update failed. Please try again later.", }); } @@ -90,21 +90,21 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await removeComment(workspaceSlug, projectId, issueId, commentId); - setToastAlert({ + setToast({ title: "Comment removed successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment removed successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment remove failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment remove failed. Please try again later.", }); } }, }), - [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment, setToastAlert] + [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment] ); const project = getProjectById(projectId); diff --git a/web/components/issues/issue-detail/label/create-label.tsx b/web/components/issues/issue-detail/label/create-label.tsx index 72bc034f872..8a6eea17ed0 100644 --- a/web/components/issues/issue-detail/label/create-label.tsx +++ b/web/components/issues/issue-detail/label/create-label.tsx @@ -5,9 +5,8 @@ import { TwitterPicker } from "react-color"; import { Popover, Transition } from "@headlessui/react"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Input } from "@plane/ui"; +import { Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TLabelOperations } from "./root"; import { IIssueLabel } from "@plane/types"; @@ -28,7 +27,6 @@ const defaultValues: Partial = { export const LabelCreate: FC = (props) => { const { workspaceSlug, projectId, issueId, labelOperations, disabled = false } = props; // hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -63,9 +61,9 @@ export const LabelCreate: FC = (props) => { await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); reset(defaultValues); } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed. Please try again sometime later.", }); } diff --git a/web/components/issues/issue-detail/label/root.tsx b/web/components/issues/issue-detail/label/root.tsx index 94f9b451f28..59ce1f54ca5 100644 --- a/web/components/issues/issue-detail/label/root.tsx +++ b/web/components/issues/issue-detail/label/root.tsx @@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite"; import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; // hooks import { useIssueDetail, useLabel } from "hooks/store"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IIssueLabel, TIssue } from "@plane/types"; -import useToast from "hooks/use-toast"; export type TIssueLabel = { workspaceSlug: string; @@ -27,7 +28,6 @@ export const IssueLabel: FC = observer((props) => { // hooks const { updateIssue } = useIssueDetail(); const { createLabel } = useLabel(); - const { setToastAlert } = useToast(); const labelOperations: TLabelOperations = useMemo( () => ({ @@ -35,16 +35,10 @@ export const IssueLabel: FC = observer((props) => { try { if (onLabelUpdate) onLabelUpdate(data.label_ids || []); else await updateIssue(workspaceSlug, projectId, issueId, data); - if (!isInboxIssue) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); } @@ -53,23 +47,23 @@ export const IssueLabel: FC = observer((props) => { try { const labelResponse = await createLabel(workspaceSlug, projectId, data); if (!isInboxIssue) - setToastAlert({ + setToast({ title: "Label created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Label created successfully", }); return labelResponse; } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed", }); return error; } }, }), - [updateIssue, createLabel, setToastAlert, onLabelUpdate] + [updateIssue, createLabel, onLabelUpdate] ); return ( diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index 6c37f86f993..f1b003b99b0 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,9 +1,8 @@ import { FC, useState } from "react"; // hooks -import useToast from "hooks/use-toast"; import { useIssueDetail, useMember } from "hooks/store"; // ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; // types @@ -27,7 +26,6 @@ export const IssueLinkDetail: FC = (props) => { link: { getLinkById }, } = useIssueDetail(); const { getUserDetails } = useMember(); - const { setToastAlert } = useToast(); // state const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false); @@ -55,8 +53,8 @@ export const IssueLinkDetail: FC = (props) => { className="flex w-full items-start justify-between gap-2 cursor-pointer" onClick={() => { copyTextToClipboard(linkDetail.url); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied!", message: "Link copied to clipboard", }); diff --git a/web/components/issues/issue-detail/links/root.tsx b/web/components/issues/issue-detail/links/root.tsx index 94124085a49..672a9e35ebf 100644 --- a/web/components/issues/issue-detail/links/root.tsx +++ b/web/components/issues/issue-detail/links/root.tsx @@ -2,7 +2,8 @@ import { FC, useCallback, useMemo, useState } from "react"; import { Plus } from "lucide-react"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueLinkCreateUpdateModal } from "./create-update-link-modal"; import { IssueLinkList } from "./links"; @@ -37,24 +38,22 @@ export const IssueLinkRoot: FC = (props) => { [toggleIssueLinkModalStore] ); - const { setToastAlert } = useToast(); - const handleLinkOperations: TLinkOperations = useMemo( () => ({ create: async (data: Partial) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await createLink(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ message: "The link has been successfully created", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link created", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be created", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not created", }); } @@ -63,16 +62,16 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await updateLink(workspaceSlug, projectId, issueId, linkId, data); - setToastAlert({ + setToast({ message: "The link has been successfully updated", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link updated", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be updated", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not updated", }); } @@ -81,22 +80,22 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeLink(workspaceSlug, projectId, issueId, linkId); - setToastAlert({ + setToast({ message: "The link has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link removed", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert, toggleIssueLinkModal] + [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, toggleIssueLinkModal] ); return ( diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index 41f4a06d609..f0fe06a2e3f 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -75,7 +75,6 @@ export const IssueModuleSelect: React.FC = observer((props) showTooltip multiple /> - {isUpdating && }
    ); }); diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 30a8621e4f4..2268540bfcc 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { ReactionSelector } from "./reaction-selector"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; import { renderEmoji } from "helpers/emoji.helper"; @@ -25,7 +26,6 @@ export const IssueCommentReaction: FC = observer((props) createCommentReaction, removeCommentReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getCommentReactionsByCommentId(commentId); const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction); @@ -36,15 +36,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields"); await createCommentReaction(workspaceSlug, projectId, commentId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -53,15 +53,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields"); removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -78,7 +78,6 @@ export const IssueCommentReaction: FC = observer((props) currentUser, createCommentReaction, removeCommentReaction, - setToastAlert, userReactions, ] ); diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index d6b33e36bb8..a9bc264f395 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { ReactionSelector } from "./reaction-selector"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; import { renderEmoji } from "helpers/emoji.helper"; @@ -24,7 +25,6 @@ export const IssueReaction: FC = observer((props) => { createReaction, removeReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getReactionsByIssueId(issueId); const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); @@ -35,15 +35,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createReaction(workspaceSlug, projectId, issueId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -52,15 +52,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields"); await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -70,7 +70,7 @@ export const IssueReaction: FC = observer((props) => { else await issueReactionOperations.create(reaction); }, }), - [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, setToastAlert, userReactions] + [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 67bba869742..26037740640 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -4,11 +4,10 @@ import { observer } from "mobx-react-lite"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks import { useIssueDetail, useIssues, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; // ui -import { RelatedIcon, Tooltip } from "@plane/ui"; +import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types @@ -60,15 +59,13 @@ export const IssueRelationSelect: React.FC = observer((pro toggleRelationModal, } = useIssueDetail(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const onSubmit = async (data: ISearchIssueResponse[]) => { if (data.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 0e343d9a87c..be0fbb74def 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -10,9 +10,10 @@ import { EmptyState } from "components/common"; import emptyIssue from "public/empty-state/issue.svg"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; @@ -21,13 +22,7 @@ import { observer } from "mobx-react"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast?: boolean - ) => Promise; + update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -76,7 +71,6 @@ export const IssueDetailRoot: FC = observer((props) => { issues: { removeIssue: removeArchivedIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); @@ -91,22 +85,9 @@ export const IssueDetailRoot: FC = observer((props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); - if (showToast) { - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); - } captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -126,9 +107,9 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); } @@ -138,9 +119,9 @@ export const IssueDetailRoot: FC = observer((props) => { let response; if (is_archived) response = await removeArchivedIssue(workspaceSlug, projectId, issueId); else response = await removeIssue(workspaceSlug, projectId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -149,9 +130,9 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); captureIssueEvent({ @@ -164,8 +145,8 @@ export const IssueDetailRoot: FC = observer((props) => { archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue archived successfully.", }); @@ -175,8 +156,8 @@ export const IssueDetailRoot: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }); @@ -189,12 +170,19 @@ export const IssueDetailRoot: FC = observer((props) => { }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setToastAlert({ - title: "Cycle added to issue successfully", - type: "success", - message: "Issue added to issue successfully", + const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setPromiseToast(addToCyclePromise, { + loading: "Adding cycle to issue...", + success: { + title: "Success!", + message: () => "Cycle added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle add to issue failed", + }, }); + await addToCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, @@ -214,21 +202,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle add to issue failed", - type: "error", - message: "Cycle add to issue failed", - }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { - const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); - setToastAlert({ - title: "Cycle removed from issue successfully", - type: "success", - message: "Cycle removed from issue successfully", + const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setPromiseToast(removeFromCyclePromise, { + loading: "Removing cycle from issue...", + success: { + title: "Success!", + message: () => "Cycle removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle remove from issue failed", + }, }); + const response = await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, @@ -248,21 +238,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle remove from issue failed", - type: "error", - message: "Cycle remove from issue failed", - }); } }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module added to issue successfully", - type: "success", - message: "Module added to issue successfully", + const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(addToModulePromise, { + loading: "Adding module to issue...", + success: { + title: "Success!", + message: () => "Module added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Module add to issue failed", + }, }); + const response = await addToModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, @@ -282,21 +274,23 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module add to issue failed", - type: "error", - message: "Module add to issue failed", - }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { - await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", + const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setPromiseToast(removeFromModulePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, }); + await removeFromModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -316,11 +310,6 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); } }, removeModulesFromIssue: async ( @@ -329,20 +318,19 @@ export const IssueDetailRoot: FC = observer((props) => { issueId: string, moduleIds: string[] ) => { - try { - await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - type: "success", - title: "Successful!", - message: "Issue removed from module successfully.", - }); - } catch (error) { - setToastAlert({ - type: "error", + const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(removeModulesFromIssuePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { title: "Error!", - message: "Issue could not be removed from module. Please try again.", - }); - } + message: () => "Module remove from issue failed", + }, + }); + await removeModulesFromIssuePromise; }, }), [ @@ -357,7 +345,6 @@ export const IssueDetailRoot: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, - setToastAlert, ] ); diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index a65cb7f1647..33dc4cdf50c 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -16,7 +16,6 @@ import { } from "lucide-react"; // hooks import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteIssueModal, @@ -30,8 +29,18 @@ import { } from "components/issues"; 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"; +// ui +import { + ArchiveIcon, + ContrastIcon, + DiceIcon, + DoubleCircleIcon, + RelatedIcon, + Tooltip, + UserGroupIcon, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -61,7 +70,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const { getProjectById } = useProject(); const { currentUser } = useUser(); const { areEstimatesEnabledForCurrentProject } = useEstimate(); - const { setToastAlert } = useToast(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -73,8 +81,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index 7321ef27f55..f4025a2f375 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -2,10 +2,9 @@ import { Bell, BellOff } from "lucide-react"; import { observer } from "mobx-react-lite"; import { FC, useState } from "react"; // UI -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; import isNil from "lodash/isNil"; export type TIssueSubscription = { @@ -22,7 +21,6 @@ export const IssueSubscription: FC = observer((props) => { createSubscription, removeSubscription, } = useIssueDetail(); - const { setToastAlert } = useToast(); // state const [loading, setLoading] = useState(false); @@ -33,16 +31,16 @@ export const IssueSubscription: FC = observer((props) => { try { if (isSubscribed) await removeSubscription(workspaceSlug, projectId, issueId); else await createSubscription(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, }); setLoading(false); } catch (error) { setLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 43f62e5bee4..fb3373a0620 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; // components import { CalendarChart } from "components/issues"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TGroupedIssues, TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; @@ -41,7 +41,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { workspaceSlug, projectId } = router.query; // hooks - const { setToastAlert } = useToast(); const { issueMap } = useIssues(); const { membership: { currentProjectRole }, @@ -73,9 +72,9 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { groupedIssueIds, viewId ).catch((err) => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: err.detail ?? "Failed to perform this action", }); }); diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 6db9323facc..c70b05c7093 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -4,13 +4,14 @@ import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; // icons import { PlusIcon } from "lucide-react"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -71,8 +72,6 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; @@ -102,13 +101,13 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); const onSubmitHandler = async (formData: TIssue) => { if (isSubmitting || !workspaceSlug || !projectId) return; @@ -120,39 +119,42 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Calendar quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - console.error(err); - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 4b767617362..b23b1998e1e 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -4,7 +4,8 @@ import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; @@ -53,16 +54,14 @@ export const CycleEmptyState: React.FC = observer((props) => { currentUser, } = useUser(); - const { setToastAlert } = useToast(); - const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; const issueIds = data.map((i) => i.id); await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index ef7ec729c07..7a5c6f57f2f 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -4,7 +4,8 @@ import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; // hooks import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; @@ -51,8 +52,6 @@ export const ModuleEmptyState: React.FC = observer((props) => { membership: { currentProjectRole: userRole }, currentUser, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -61,8 +60,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { await issues .addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the module. Please try again.", }) diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 7ed6a87306f..10b73ab355a 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -5,13 +5,14 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; import { cn } from "helpers/common.helper"; +// ui +import { setPromiseToast } from "@plane/ui"; // types import { IProject, TIssue } from "@plane/types"; // constants @@ -70,7 +71,6 @@ export const GanttQuickAddIssueForm: React.FC = observe // hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined; @@ -110,31 +110,35 @@ export const GanttQuickAddIssueForm: React.FC = observe target_date: renderFormattedPayloadDate(targetDate), }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Gantt quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, + path: router.asPath, + }); + }); } }; return ( diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7bdaf282d22..3951e7032dd 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -4,9 +4,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; @@ -80,8 +79,6 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas } = useUser(); const { captureIssueEvent } = useEventTracker(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); const issueIds = issues?.groupedIssueIds || []; @@ -159,9 +156,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas issueIds, viewId ).catch((err) => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: err.detail ?? "Failed to perform this action", }); }); diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index f49af292217..d2f1febd71f 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -1,13 +1,13 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CustomMenu } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal } from "components/issues"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // mobx import { observer } from "mobx-react-lite"; @@ -56,8 +56,6 @@ export const HeaderGroupByCard: FC = observer((props) => { const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -69,8 +67,8 @@ export const HeaderGroupByCard: FC = observer((props) => { try { addIssuesToView && addIssuesToView(issues); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 51316343182..20f0cd8e0d7 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -5,11 +5,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; +// ui +import { setPromiseToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -73,7 +74,6 @@ export const KanBanQuickAddIssueForm: React.FC = obser useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -97,39 +97,42 @@ export const KanBanQuickAddIssueForm: React.FC = obser ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Kanban quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 8d9164b3767..404107af4a0 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -4,14 +4,14 @@ import { CircleDashed, Plus } from "lucide-react"; // components import { CreateUpdateIssueModal } from "components/issues"; import { ExistingIssuesListModal } from "components/core"; -import { CustomMenu } from "@plane/ui"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // mobx import { observer } from "mobx-react-lite"; // hooks import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; -import useToast from "hooks/use-toast"; import { useState } from "react"; import { TCreateModalStoreTypes } from "constants/issue"; @@ -38,8 +38,6 @@ export const HeaderGroupByCard = observer( const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -51,8 +49,8 @@ export const HeaderGroupByCard = observer( try { addIssuesToView && addIssuesToView(issues); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 8d1ce6d9c54..3c71293b424 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -5,12 +5,13 @@ import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// constants -import { TIssue, IProject } from "@plane/types"; +// ui +import { setPromiseToast } from "@plane/ui"; // types +import { TIssue, IProject } from "@plane/types"; +// helper import { createIssuePayload } from "helpers/issue.helper"; // constants import { ISSUE_CREATED } from "constants/event-tracker"; @@ -77,7 +78,6 @@ export const ListQuickAddIssueForm: FC = observer((props useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -101,31 +101,35 @@ export const ListQuickAddIssueForm: FC = observer((props ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "List quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "List quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "List quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 20d21dc5f62..c2ac29eef2c 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useProjectState } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -39,8 +39,6 @@ export const AllIssueQuickActions: React.FC = observer((props // store hooks const { setTrackElement } = useEventTracker(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); const isEditingAllowed = !readOnly; @@ -54,8 +52,8 @@ export const AllIssueQuickActions: React.FC = observer((props const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 01e9b492131..a30db3a8295 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -1,10 +1,10 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useUser } from "hooks/store"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { DeleteIssueModal } from "components/issues"; // helpers @@ -32,16 +32,14 @@ export const ArchivedIssueQuickActions: React.FC = (props) => // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; const isRestoringAllowed = handleRestore && isEditingAllowed; - // toast alert - const { setToastAlert } = useToast(); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 8cd5dd56f9a..2b4a5fa05c9 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,12 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// icons +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -45,8 +46,6 @@ export const CycleIssueQuickActions: React.FC = observer((pro membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +63,8 @@ export const CycleIssueQuickActions: React.FC = observer((pro const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 13db0e9f1db..cf090385dfa 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks -import useToast from "hooks/use-toast"; import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -45,8 +45,6 @@ export const ModuleIssueQuickActions: React.FC = observer((pr membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +62,8 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 99a8e60196b..7afbd24210a 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; // hooks import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -54,16 +54,14 @@ export const ProjectIssueQuickActions: React.FC = observer((p !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; - const { setToastAlert } = useToast(); - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 3cba3c6cdb8..4b0d7aed413 100644 --- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -5,11 +5,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants @@ -84,7 +85,6 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // hooks useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); useEffect(() => { setFocus("name"); @@ -100,13 +100,13 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); // const onSubmitHandler = async (formData: TIssue) => { // if (isSubmitting || !workspaceSlug || !projectId) return; @@ -130,8 +130,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // payload // ); - // setToastAlert({ - // type: "success", + // setToast({ + // type: TOAST_TYPE.SUCCESS, // title: "Success!", // message: "Issue created successfully.", // }); @@ -140,8 +140,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // const error = err?.[key]; // const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - // setToastAlert({ - // type: "error", + // setToast({ + // type: TOAST_TYPE.ERROR, // title: "Error!", // message: errorTitle || "Some error occurred. Please try again.", // }); @@ -159,34 +159,41 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then( - (res) => { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - } - )); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + currentWorkspace.slug, + currentProjectDetails.id, + { ...payload } as TIssue, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, }); + + await quickAddPromise + .then((res) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + }) + .catch((err) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + console.error(err); + }); } }; diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 1f9935c989e..b4dae211d7d 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -2,10 +2,11 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // services import { IssueDraftService } from "services/issue"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { IssueFormRoot } from "components/issues/issue-modal/form"; import { ConfirmIssueDiscard } from "components/issues"; @@ -43,8 +44,6 @@ export const DraftIssueLayout: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureIssueEvent } = useEventTracker(); @@ -61,8 +60,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Draft Issue created successfully.", }); @@ -76,8 +75,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index e2e4e784e8f..7fcb6cffa62 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -7,7 +7,6 @@ import { LayoutPanelTop, Sparkle, X } from "lucide-react"; import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; import { FileService } from "services/file.service"; @@ -27,7 +26,7 @@ import { StateDropdown, } from "components/dropdowns"; // ui -import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui"; +import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types @@ -125,8 +124,6 @@ export const IssueFormRoot: FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); - // toast alert - const { setToastAlert } = useToast(); // form info const { formState: { errors, isDirty, isSubmitting }, @@ -199,8 +196,8 @@ export const IssueFormRoot: FC = observer((props) => { }) .then((res) => { if (res.response === "") - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue title isn't informative enough to generate the description. Please try with a different title.", @@ -211,14 +208,14 @@ export const IssueFormRoot: FC = observer((props) => { const error = err?.data?.error; if (err.status === 429) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "You have reached the maximum number of requests of 50 requests per month per user.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "Some error occurred. Please try again.", }); diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 1da02f0acd1..3b97f9c4e3e 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -13,11 +13,12 @@ import { useWorkspace, useIssueDetail, } from "hooks/store"; -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { TIssue } from "@plane/types"; // constants @@ -89,8 +90,6 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }; // router const router = useRouter(); - // toast alert - const { setToastAlert } = useToast(); // local storage const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage< Record> @@ -186,9 +185,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop await addIssueToCycle(response, payload.cycle_id); if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE) await addIssueToModule(response, payload.module_ids); - - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue created successfully.", }); @@ -200,8 +198,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop !createMore && handleClose(); return response; } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); @@ -221,8 +219,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop ? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload) : await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue updated successfully.", }); @@ -233,8 +231,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }); handleClose(); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8db7fd0ace2..8d8ec00df3b 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -3,11 +3,18 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui -import { ArchiveIcon, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; +import { + ArchiveIcon, + CenterPanelIcon, + CustomSelect, + FullScreenPanelIcon, + SidePanelIcon, + Tooltip, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; -// hooks -import useToast from "hooks/use-toast"; // store hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // helpers @@ -74,8 +81,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - // hooks - const { setToastAlert } = useToast(); // derived values const issueDetails = getIssueById(issueId); const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined; @@ -87,8 +92,8 @@ export const IssuePeekOverviewHeader: FC = observer((pr e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(issueLink).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 466bffee7a0..b28cc5de60f 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -2,8 +2,9 @@ import { FC, useEffect, useState, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components import { IssueView } from "components/issues"; // types @@ -20,13 +21,7 @@ interface IIssuePeekOverview { export type TIssuePeekOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast?: boolean - ) => Promise; + update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -49,8 +44,6 @@ export type TIssuePeekOperations = { export const IssuePeekOverview: FC = observer((props) => { const { is_archived = false, is_draft = false } = props; - // hooks - const { setToastAlert } = useToast(); // router const router = useRouter(); const { @@ -86,49 +79,38 @@ export const IssuePeekOverview: FC = observer((props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { - try { - await updateIssue(workspaceSlug, projectId, issueId, data); - if (showToast) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + await updateIssue(workspaceSlug, projectId, issueId, data) + .then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, + updates: { + changed_property: Object.keys(data).join(","), + change_details: Object.values(data).join(","), + }, + path: router.asPath, + }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { state: "FAILED", element: "Issue peek-overview" }, + path: router.asPath, + }); + setToast({ + title: "Issue update failed", + type: TOAST_TYPE.ERROR, + message: "Issue update failed", }); - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, - updates: { - changed_property: Object.keys(data).join(","), - change_details: Object.values(data).join(","), - }, - path: router.asPath, - }); - } catch (error) { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { state: "FAILED", element: "Issue peek-overview" }, - path: router.asPath, - }); - setToastAlert({ - title: "Issue update failed", - type: "error", - message: "Issue update failed", }); - } }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { removeIssue(workspaceSlug, projectId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -137,9 +119,9 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); captureIssueEvent({ @@ -152,8 +134,8 @@ export const IssuePeekOverview: FC = observer((props) => { archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue archived successfully.", }); @@ -163,8 +145,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }); @@ -178,8 +160,8 @@ export const IssuePeekOverview: FC = observer((props) => { restore: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await restoreIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue restored successfully.", }); @@ -189,8 +171,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be restored. Please try again.", }); @@ -203,12 +185,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setToastAlert({ - title: "Cycle added to issue successfully", - type: "success", - message: "Issue added to issue successfully", + const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setPromiseToast(addToCyclePromise, { + loading: "Adding cycle to issue...", + success: { + title: "Success!", + message: () => "Cycle added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle add to issue failed", + }, }); + await addToCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, @@ -228,21 +217,23 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle add to issue failed", - type: "error", - message: "Cycle add to issue failed", - }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { - const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); - setToastAlert({ - title: "Cycle removed from issue successfully", - type: "success", - message: "Cycle removed from issue successfully", + const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setPromiseToast(removeFromCyclePromise, { + loading: "Removing cycle from issue...", + success: { + title: "Success!", + message: () => "Cycle removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle remove from issue failed", + }, }); + const response = await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -253,11 +244,6 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - title: "Cycle remove from issue failed", - type: "error", - message: "Cycle remove from issue failed", - }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, @@ -271,12 +257,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module added to issue successfully", - type: "success", - message: "Module added to issue successfully", + const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(addToModulePromise, { + loading: "Adding module to issue...", + success: { + title: "Success!", + message: () => "Module added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Module add to issue failed", + }, }); + const response = await addToModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -296,21 +289,23 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module add to issue failed", - type: "error", - message: "Module add to issue failed", - }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { - await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", + const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setPromiseToast(removeFromModulePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, }); + await removeFromModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, @@ -330,11 +325,6 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); } }, removeModulesFromIssue: async ( @@ -343,20 +333,19 @@ export const IssuePeekOverview: FC = observer((props) => { issueId: string, moduleIds: string[] ) => { - try { - await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", - }); - } catch (error) { - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); - } + const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(removeModulesFromIssuePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, + }); + await removeModulesFromIssuePromise; }, }), [ @@ -372,7 +361,6 @@ export const IssuePeekOverview: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, - setToastAlert, captureIssueEvent, router.asPath, ] diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index cb2100ce03d..f94901c454f 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -5,7 +5,6 @@ import { observer } from "mobx-react-lite"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useKeypress from "hooks/use-keypress"; -import useToast from "hooks/use-toast"; // store hooks import { useIssueDetail } from "hooks/store"; // components @@ -50,15 +49,13 @@ export const IssueView: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const issue = getIssueById(issueId); - // hooks - const { alerts } = useToast(); // remove peek id const removeRoutePeekId = () => { setPeekIssue(undefined); }; useOutsideClickDetector(issuePeekOverviewRef, () => { - if (!isAnyModalOpen && (!alerts || alerts.length === 0)) { + if (!isAnyModalOpen) { removeRoutePeekId(); } }); diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 5e406116c45..da49200dd07 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -4,14 +4,13 @@ import { observer } from "mobx-react-lite"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; // hooks import { useEventTracker, useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { IssueList } from "./issues-list"; import { ProgressBar } from "./progressbar"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -46,8 +45,6 @@ export const SubIssuesRoot: FC = observer((props) => { const { workspaceSlug, projectId, parentIssueId, disabled = false } = props; // router const router = useRouter(); - // store hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers }, @@ -128,8 +125,8 @@ export const SubIssuesRoot: FC = observer((props) => { copyText: (text: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${text}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); @@ -139,8 +136,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { await fetchSubIssues(workspaceSlug, projectId, parentIssueId); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error fetching sub-issues", message: "Error fetching sub-issues", }); @@ -149,14 +146,14 @@ export const SubIssuesRoot: FC = observer((props) => { addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => { try { await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issues added successfully", message: "Sub-issues added successfully", }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error adding sub-issue", message: "Error adding sub-issue", }); @@ -183,8 +180,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue updated successfully", message: "Sub-issue updated successfully", }); @@ -199,8 +196,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error updating sub-issue", message: "Error updating sub-issue", }); @@ -210,8 +207,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue removed successfully", message: "Sub-issue removed successfully", }); @@ -235,8 +232,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error removing sub-issue", message: "Error removing sub-issue", }); @@ -246,8 +243,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issue deleted successfully", message: "Issue deleted successfully", }); @@ -263,15 +260,15 @@ export const SubIssuesRoot: FC = observer((props) => { payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error deleting issue", message: "Error deleting issue", }); } }, }), - [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setToastAlert, setSubIssueHelpers] + [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers] ); const issue = getIssueById(parentIssueId); diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 4a4057a6ac6..2db4eb4b5ff 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -32,7 +32,7 @@ export const IssueTitleInput: FC = observer((props) => { useEffect(() => { const textarea = document.querySelector("#title-input"); if (debouncedValue && debouncedValue !== value) { - issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { + issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }).finally(() => { setIsSubmitting("saved"); if (textarea && !textarea.matches(":focus")) { const trimmedTitle = debouncedValue.trim(); diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index e53e9114744..b6a3f63e87d 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -7,9 +7,8 @@ import { Dialog, Popover, Transition } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel, IState } from "@plane/types"; // constants @@ -64,8 +63,6 @@ export const CreateLabelModal: React.FC = observer((props) => { reset(defaultValues); }; - const { setToastAlert } = useToast(); - const onSubmit = async (formData: IIssueLabel) => { if (!workspaceSlug) return; @@ -75,9 +72,9 @@ export const CreateLabelModal: React.FC = observer((props) => { if (onSuccess) onSuccess(res); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 2d2be046dcd..d30d48a6ac9 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -6,9 +6,8 @@ import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; // hooks import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // fetch-keys @@ -35,8 +34,6 @@ export const CreateUpdateLabelInline = observer( const { workspaceSlug, projectId } = router.query; // store hooks const { createLabel, updateLabel } = useLabel(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -65,9 +62,9 @@ export const CreateUpdateLabelInline = observer( reset(defaultValues); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); @@ -83,9 +80,9 @@ export const CreateUpdateLabelInline = observer( handleClose(); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while updating the label", }); reset(formData); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index 64d15eb65cb..83b3e807d88 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -6,10 +6,8 @@ import { observer } from "mobx-react-lite"; import { useLabel } from "hooks/store"; // icons import { AlertTriangle } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel } from "@plane/types"; @@ -28,8 +26,6 @@ export const DeleteLabelModal: React.FC = observer((props) => { const { deleteLabel } = useLabel(); // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // hooks - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -49,8 +45,8 @@ export const DeleteLabelModal: React.FC = observer((props) => { setIsDeleteLoading(false); const error = err?.error || "Label could not be deleted. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error, }); diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index bf2e529b74c..de9571179d8 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -4,9 +4,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useModule } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { AlertTriangle } from "lucide-react"; // types @@ -30,8 +29,6 @@ export const DeleteModuleModal: React.FC = observer((props) => { // store hooks const { captureModuleEvent } = useEventTracker(); const { deleteModule } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -47,8 +44,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { .then(() => { if (moduleId || peekModule) router.push(`/${workspaceSlug}/projects/${data.project_id}/modules`); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module deleted successfully.", }); @@ -58,8 +55,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Module could not be deleted. Please try again.", }); diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index 47f3313964b..00781affeea 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -4,7 +4,8 @@ import { useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; // hooks import { useEventTracker, useModule, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ModuleForm } from "components/modules"; // types @@ -36,8 +37,6 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { const { captureModuleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createModule, updateModuleDetails } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { reset(defaultValues); @@ -55,8 +54,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { await createModule(workspaceSlug.toString(), selectedProjectId, payload) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module created successfully.", }); @@ -66,8 +65,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be created. Please try again.", }); @@ -86,8 +85,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); @@ -97,8 +96,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be updated. Please try again.", }); diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 4dec3df6eed..52cc6097be3 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // hooks import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui -import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip } from "@plane/ui"; +import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -30,8 +29,6 @@ export const ModuleCardItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +45,27 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +73,37 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 3d7468f24a0..72ed16adf1e 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -5,11 +5,19 @@ import { observer } from "mobx-react-lite"; import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // hooks import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui -import { Avatar, AvatarGroup, CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui"; +import { + Avatar, + AvatarGroup, + CircularProgressIndicator, + CustomMenu, + Tooltip, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -30,8 +38,6 @@ export const ModuleListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +54,27 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +82,37 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index c8c55321c8c..ad3da373cd5 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -16,14 +16,22 @@ import { } from "lucide-react"; // hooks import { useModule, useUser, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; // ui -import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui"; +import { + CustomMenu, + Loader, + LayersIcon, + CustomSelect, + ModuleStatusIcon, + UserGroupIcon, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; @@ -65,8 +73,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); - const { setToastAlert } = useToast(); - const { reset, control } = useForm({ defaultValues, }); @@ -99,15 +105,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link created", message: "Module link created successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -125,15 +131,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link updated", message: "Module link updated successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -149,15 +155,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link deleted", message: "Module link deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -167,15 +173,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Module link copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -187,8 +193,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { start_date: startDate ? renderFormattedPayloadDate(startDate) : null, target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 03d849a8219..bd26dcfa588 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -5,10 +5,9 @@ import Link from "next/link"; import { Menu } from "@headlessui/react"; import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // icons -import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { snoozeOptions } from "constants/notification"; // helper @@ -50,8 +49,6 @@ export const NotificationCard: React.FC = (props) => { const { workspaceSlug } = router.query; // states const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false); - // toast alert - const { setToastAlert } = useToast(); // refs const snoozeRef = useRef(null); @@ -62,9 +59,9 @@ export const NotificationCard: React.FC = (props) => { icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -79,9 +76,9 @@ export const NotificationCard: React.FC = (props) => { ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -94,9 +91,9 @@ export const NotificationCard: React.FC = (props) => { return; } markSnoozeNotification(notification.id, date).then(() => { - setToastAlert({ + setToast({ title: `Notification snoozed till ${renderFormattedDate(date)}`, - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; @@ -330,9 +327,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -352,9 +349,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -403,9 +400,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: `Notification snoozed till ${renderFormattedDate(item.value)}`, - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }} diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index f89b3a96345..c2875b8dd4c 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -6,10 +6,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; // constants import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect } from "@plane/ui"; +import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -41,8 +39,6 @@ export const SnoozeNotificationModal: FC = (props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { formState: { isSubmitting }, reset, @@ -100,10 +96,10 @@ export const SnoozeNotificationModal: FC = (props) => { await handleSubmitSnooze(notification.id, dateTime).then(() => { handleClose(); onSuccess(); - setToastAlert({ + setToast({ title: "Notification snoozed", message: "Notification snoozed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 561a428d626..1f78fcf204c 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -17,10 +17,9 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; // services import { WorkspaceService } from "services/workspace.service"; // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { OnboardingStepIndicator } from "components/onboarding/step-indicator"; // hooks @@ -269,7 +268,6 @@ export const InviteMembers: React.FC = (props) => { const [isInvitationDisabled, setIsInvitationDisabled] = useState(true); - const { setToastAlert } = useToast(); const { resolvedTheme } = useTheme(); // store hooks const { captureEvent } = useEventTracker(); @@ -322,8 +320,8 @@ export const InviteMembers: React.FC = (props) => { state: "SUCCESS", element: "Onboarding", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -336,8 +334,8 @@ export const InviteMembers: React.FC = (props) => { state: "FAILED", element: "Onboarding", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }); diff --git a/web/components/onboarding/switch-delete-account-modal.tsx b/web/components/onboarding/switch-delete-account-modal.tsx index 66b98fb2355..ff37e5802b4 100644 --- a/web/components/onboarding/switch-delete-account-modal.tsx +++ b/web/components/onboarding/switch-delete-account-modal.tsx @@ -6,7 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -25,8 +26,6 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { const { resolvedTheme, setTheme } = useTheme(); - const { setToastAlert } = useToast(); - const handleClose = () => { setSwitchingAccount(false); setIsDeactivating(false); @@ -44,8 +43,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) @@ -58,8 +57,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deleted successfully.", }); @@ -69,8 +68,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) diff --git a/web/components/onboarding/workspace.tsx b/web/components/onboarding/workspace.tsx index ad9342e3aef..5ba5ca81c5e 100644 --- a/web/components/onboarding/workspace.tsx +++ b/web/components/onboarding/workspace.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; import { Control, Controller, FieldErrors, UseFormHandleSubmit, UseFormSetValue } from "react-hook-form"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // services import { WorkspaceService } from "services/workspace.service"; // constants @@ -35,8 +34,6 @@ export const Workspace: React.FC = (props) => { const { updateCurrentUser } = useUser(); const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace(); const { captureWorkspaceEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleCreateWorkspace = async (formData: IWorkspace) => { if (isSubmitting) return; @@ -49,8 +46,8 @@ export const Workspace: React.FC = (props) => { await createWorkspace(formData) .then(async (res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -75,8 +72,8 @@ export const Workspace: React.FC = (props) => { element: "Onboarding", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -84,8 +81,8 @@ export const Workspace: React.FC = (props) => { } else setSlugError(true); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }) diff --git a/web/components/pages/delete-page-modal.tsx b/web/components/pages/delete-page-modal.tsx index bba19b31c95..67cd175f004 100644 --- a/web/components/pages/delete-page-modal.tsx +++ b/web/components/pages/delete-page-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, usePage } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { useProjectPages } from "hooks/store/use-project-page"; // constants @@ -32,9 +31,6 @@ export const DeletePageModal: React.FC = observer((pr const { capturePageEvent } = useEventTracker(); const pageStore = usePage(pageId); - // toast alert - const { setToastAlert } = useToast(); - if (!pageStore) return null; const { name } = pageStore; @@ -60,8 +56,8 @@ export const DeletePageModal: React.FC = observer((pr }, }); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Page deleted successfully.", }); @@ -74,8 +70,8 @@ export const DeletePageModal: React.FC = observer((pr state: "FAILED", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Page could not be deleted. Please try again.", }); diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx index e041b28d8f3..fd158e2d5a4 100644 --- a/web/components/profile/preferences/email-notification-form.tsx +++ b/web/components/profile/preferences/email-notification-form.tsx @@ -1,9 +1,7 @@ import React, { FC } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Checkbox } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, Checkbox, TOAST_TYPE, setToast } from "@plane/ui"; // services import { UserService } from "services/user.service"; // types @@ -18,8 +16,6 @@ const userService = new UserService(); export const EmailNotificationForm: FC = (props) => { const { data } = props; - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -45,9 +41,9 @@ export const EmailNotificationForm: FC = (props) => await userService .updateCurrentUserEmailNotificationSettings(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Email Notification Settings updated successfully", }) ) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 3d6f62b7b8d..9f554cfea8f 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -5,11 +5,10 @@ import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; import Link from "next/link"; // hooks import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteProjectModal, JoinProjectModal } from "components/project"; // ui -import { Avatar, AvatarGroup, Button, Tooltip } from "@plane/ui"; +import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; @@ -27,8 +26,6 @@ export const ProjectCard: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // states const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false); const [joinProjectModalOpen, setJoinProjectModal] = useState(false); @@ -42,24 +39,34 @@ export const ProjectCard: React.FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug) return; - addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(addToFavoritePromise, { + loading: "Adding project to favorites...", + success: { + title: "Success!", + message: () => "Project added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't add the project to favorites. Please try again.", + }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; - removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing project from favorites...", + success: { + title: "Success!", + message: () => "Project removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't remove the project from favorites. Please try again.", + }, }); }; @@ -67,8 +74,8 @@ export const ProjectCard: React.FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${project.id}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 49a42a0a3a3..f7bbd92cf24 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks import { useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; @@ -32,16 +31,14 @@ interface IIsGuestCondition { } const IsGuestCondition: FC = ({ onClose }) => { - const { setToastAlert } = useToast(); - useEffect(() => { onClose(); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "You don't have permission to create project.", }); - }, [onClose, setToastAlert]); + }, [onClose]); return null; }; @@ -69,8 +66,6 @@ export const CreateProjectModal: FC = observer((props) => { const { addProjectToFavorites, createProject } = useProject(); // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); - // toast - const { setToastAlert } = useToast(); // form info const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { @@ -108,8 +103,8 @@ export const CreateProjectModal: FC = observer((props) => { if (!workspaceSlug) return; addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Couldn't remove the project from favorites. Please try again.", }); @@ -137,8 +132,8 @@ export const CreateProjectModal: FC = observer((props) => { eventName: PROJECT_CREATED, payload: newPayload, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project created successfully.", }); @@ -149,8 +144,8 @@ export const CreateProjectModal: FC = observer((props) => { }) .catch((err) => { Object.keys(err.data).map((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.data[key], }); diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 791ac3672ca..844bd3aadc4 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IProject } from "@plane/types"; // constants @@ -33,8 +32,6 @@ export const DeleteProjectModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -67,8 +64,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "SUCCESS", element: "Project general settings" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project deleted successfully.", }); @@ -78,8 +75,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 267103dc8ba..ef5a20024a0 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -2,11 +2,10 @@ import { FC, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // hooks import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import EmojiIconPicker from "components/emoji-icon-picker"; import { ImagePickerPopover } from "components/core"; -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Lock } from "lucide-react"; // types @@ -33,8 +32,6 @@ export const ProjectDetailsForm: FC = (props) => { // store hooks const { captureProjectEvent } = useEventTracker(); const { updateProject } = useProject(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -84,8 +81,8 @@ export const ProjectDetailsForm: FC = (props) => { element: "Project general settings", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project updated successfully", }); @@ -95,8 +92,8 @@ export const ProjectDetailsForm: FC = (props) => { eventName: PROJECT_UPDATED, payload: { ...payload, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Project could not be updated. Please try again.", }); diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index d2910b34a3d..cf256098f1e 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -8,12 +8,13 @@ import useSWR, { mutate } from "swr"; import { ProjectService } from "services/project"; // hooks import { useRouter } from "next/router"; -import useToast from "hooks/use-toast"; // components import { SelectRepository, SelectChannel } from "components/integration"; // icons import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceIntegration } from "@plane/types"; // fetch-keys @@ -41,8 +42,6 @@ export const IntegrationCard: React.FC = ({ integration }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); - const { data: syncedGithubRepository } = useSWR( projectId ? PROJECT_GITHUB_REPOSITORY(projectId as string) : null, () => @@ -71,16 +70,16 @@ export const IntegrationCard: React.FC = ({ integration }) => { .then(() => { mutate(PROJECT_GITHUB_REPOSITORY(projectId as string)); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: `${login}/${name} repository synced with the project successfully.`, }); }) .catch((err) => { console.error(err); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Repository could not be synced with the project. Please try again.", }); diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 0827568ce50..45618d4f246 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -6,9 +6,8 @@ import { AlertTriangleIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject } from "@plane/types"; // constants @@ -40,8 +39,6 @@ export const LeaveProjectModal: FC = observer((props) => { const { membership: { leaveProject }, } = useUser(); - // toast - const { setToastAlert } = useToast(); const { control, @@ -71,8 +68,8 @@ export const LeaveProjectModal: FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong please try again later.", }); @@ -82,22 +79,22 @@ export const LeaveProjectModal: FC = observer((props) => { }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please confirm leaving the project by typing the 'Leave Project'.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter the project name as shown in the description.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please fill all fields.", }); diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 6a27eccd512..6bab775b8c0 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -4,11 +4,10 @@ import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmProjectMemberRemove } from "components/project"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; // constants @@ -37,8 +36,6 @@ export const ProjectMemberListItem: React.FC = observer((props) => { project: { removeMemberFromProject, getProjectMemberDetails, updateMember }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; @@ -58,8 +55,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { router.push(`/${workspaceSlug}/projects`); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -67,8 +64,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { } else await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch( (err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -151,8 +148,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const error = err.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index b4713f7395b..91c06cdfcca 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -4,12 +4,11 @@ import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks import { useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; import { Controller, useForm } from "react-hook-form"; import { MemberSelect } from "components/project"; // ui -import { Loader } from "@plane/ui"; +import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject, IUserLite, IWorkspace } from "@plane/types"; // fetch-keys @@ -33,8 +32,6 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; - // hooks - const { setToastAlert } = useToast(); // form info const { reset, control } = useForm({ defaultValues }); // fetching user members @@ -72,9 +69,9 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { }) .then(() => { fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Project updated successfully", }); }) diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 77be4c84b27..64cf87fb5c2 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { Check, CircleDot, Globe2 } from "lucide-react"; // hooks import { useProjectPublish } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Loader, ToggleSwitch } from "@plane/ui"; +import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomPopover } from "./popover"; // types import { IProject } from "@plane/types"; @@ -71,8 +70,6 @@ export const PublishProjectModal: React.FC = observer((props) => { unPublishProject, fetchSettingsLoader, } = useProjectPublish(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -150,8 +147,8 @@ export const PublishProjectModal: React.FC = observer((props) => { await updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Publish settings updated successfully!", }); @@ -176,8 +173,8 @@ export const PublishProjectModal: React.FC = observer((props) => { return res; }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while un-publishing the project.", }) @@ -208,8 +205,8 @@ export const PublishProjectModal: React.FC = observer((props) => { const handleFormSubmit = async (formData: FormData) => { if (!formData.views || formData.views.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one view layout to publish the project.", }); diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index ef7913fb08c..da2f37e9f94 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Avatar, Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; +import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { getUserRole } from "helpers/user.helper"; // constants @@ -45,8 +44,6 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureEvent } = useEventTracker(); const { @@ -84,9 +81,9 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { .then(() => { if (onSuccess) onSuccess(); onClose(); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Members added successfully.", }); captureEvent(PROJECT_MEMBER_ADDED, { diff --git a/web/components/project/settings/features-list.tsx b/web/components/project/settings/features-list.tsx index 22e69827efa..efbcc085765 100644 --- a/web/components/project/settings/features-list.tsx +++ b/web/components/project/settings/features-list.tsx @@ -2,10 +2,10 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react"; -import { DiceIcon, ToggleSwitch } from "@plane/ui"; // hooks import { useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProject } from "@plane/types"; // constants @@ -58,13 +58,11 @@ export const ProjectFeaturesList: FC = observer(() => { } = useUser(); const { currentProjectDetails, updateProject } = useProject(); const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; - // toast alert - const { setToastAlert } = useToast(); const handleSubmit = async (formData: Partial) => { if (!workspaceSlug || !projectId || !currentProjectDetails) return; - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project feature updated successfully.", }); diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 695e0bce4f9..00dc858d0c5 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -21,13 +21,22 @@ import { // hooks import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useToast from "hooks/use-toast"; // helpers import { cn } from "helpers/common.helper"; import { getNumberCount } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; +// ui +import { + CustomMenu, + Tooltip, + ArchiveIcon, + PhotoFilterIcon, + DiceIcon, + ContrastIcon, + LayersIcon, + setPromiseToast, +} from "@plane/ui"; // components -import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui"; import { LeaveProjectModal, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; @@ -93,8 +102,6 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId: URLProjectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // derived values const project = getProjectById(projectId); @@ -112,24 +119,34 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; - addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(addToFavoritePromise, { + loading: "Adding project to favorites...", + success: { + title: "Success!", + message: () => "Project added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't add the project to favorites. Please try again.", + }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; - removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing project from favorites...", + success: { + title: "Success!", + message: () => "Project removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't remove the project from favorites. Please try again.", + }, }); }; diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 983e23932c7..05e09f565de 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -6,7 +6,8 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers @@ -42,15 +43,13 @@ export const ProjectSidebarList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const handleCopyText = (projectId: string) => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); @@ -72,8 +71,8 @@ export const ProjectSidebarList: FC = observer(() => { const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList); if (updatedSortOrder != undefined) updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index db91bb6b058..f39e3f33585 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown } from "lucide-react"; // types @@ -37,8 +36,6 @@ export const CreateStateModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { createState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { formState: { errors, isSubmitting }, @@ -71,15 +68,15 @@ export const CreateStateModal: React.FC = observer((props) => { if (typeof error === "object") { Object.keys(error).forEach((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: Array.isArray(error[key]) ? error[key].join(", ") : error[key], }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error ?? err.status === 400 diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index 037cd483d90..0a50208cd1f 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -6,9 +6,8 @@ import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, Tooltip } from "@plane/ui"; +import { Button, CustomSelect, Input, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IState } from "@plane/types"; // constants @@ -39,8 +38,6 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); const { createState, updateState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -82,8 +79,8 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { await createState(workspaceSlug.toString(), projectId.toString(), formData) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State created successfully.", }); @@ -98,14 +95,14 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State with that name already exists. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be created. Please try again.", }); @@ -135,22 +132,22 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { element: "Project settings states page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State updated successfully.", }); }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Another state exists with the same name. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be updated. Please try again.", }); diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index 12de3860872..df47c8b12ef 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IState } from "@plane/types"; // constants @@ -29,8 +28,6 @@ export const DeleteStateModal: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent } = useEventTracker(); const { deleteState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -55,15 +52,15 @@ export const DeleteStateModal: React.FC = observer((props) => { }) .catch((err) => { if (err.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "This state contains some issues within it, please move them to some other state to delete this state.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be deleted. Please try again.", }); diff --git a/web/components/toast-alert/index.tsx b/web/components/toast-alert/index.tsx deleted file mode 100644 index b4df6ea052b..00000000000 --- a/web/components/toast-alert/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -// hooks -import useToast from "hooks/use-toast"; -// icons -import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react"; - -const ToastAlerts = () => { - const { alerts, removeAlert } = useToast(); - - if (!alerts) return null; - - return ( -
    - {alerts.map((alert) => ( -
    -
    - -
    -
    -
    -
    - {alert.type === "success" ? ( -
    -
    -

    {alert.title}

    - {alert.message &&

    {alert.message}

    } -
    -
    -
    -
    - ))} -
    - ); -}; - -export default ToastAlerts; diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 5bd477352b0..f4fe8e12051 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -5,9 +5,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IProjectView } from "@plane/types"; @@ -26,8 +25,6 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -43,15 +40,15 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View deleted successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be deleted. Please try again.", }) diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 43cea7d5c94..a1abef1a4a0 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // hooks import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ProjectViewForm } from "components/views"; // types @@ -22,8 +23,6 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props; // store hooks const { createView, updateView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -33,15 +32,15 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await createView(workspaceSlug, projectId, payload) .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) @@ -52,8 +51,8 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await updateView(workspaceSlug, projectId, data?.id as string, payload) .then(() => handleClose()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Something went wrong. Please try again.", }) diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 48cc12ada36..7ff1ee92e48 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; // hooks import { useProjectView, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; @@ -30,8 +29,6 @@ export const ProjectViewListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -54,8 +51,8 @@ export const ProjectViewListItem: React.FC = observer((props) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/views/${view.id}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "View link copied to clipboard.", }); diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx index f8301bf53da..ecbd4ccd3ff 100644 --- a/web/components/web-hooks/create-webhook-modal.tsx +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -5,11 +5,12 @@ import { Dialog, Transition } from "@headlessui/react"; import { WebhookForm } from "./form"; import { GeneratedHookDetails } from "./generated-hook-details"; // hooks -import useToast from "hooks/use-toast"; // helpers import { csvDownload } from "helpers/download.helper"; // utils import { getCurrentHookAsCSV } from "./utils"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; @@ -34,8 +35,6 @@ export const CreateWebhookModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const handleCreateWebhook = async (formData: IWebhook, webhookEventType: TWebhookEventTypes) => { if (!workspaceSlug) return; @@ -65,8 +64,8 @@ export const CreateWebhookModal: React.FC = (props) => { await createWebhook(workspaceSlug.toString(), payload) .then(({ webHook, secretKey }) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook created successfully.", }); @@ -77,8 +76,8 @@ export const CreateWebhookModal: React.FC = (props) => { csvDownload(csvData, `webhook-secret-key-${Date.now()}`); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx index 6cc30bb5796..52c7a6595e8 100644 --- a/web/components/web-hooks/delete-webhook-modal.tsx +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -4,9 +4,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useWebhook } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; interface IDeleteWebhook { isOpen: boolean; @@ -19,8 +18,6 @@ export const DeleteWebhookModal: FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); // router const router = useRouter(); - // toast - const { setToastAlert } = useToast(); // store hooks const { removeWebhook } = useWebhook(); @@ -37,16 +34,16 @@ export const DeleteWebhookModal: FC = (props) => { removeWebhook(workspaceSlug.toString(), webhookId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook deleted successfully.", }); router.replace(`/${workspaceSlug}/settings/webhooks/`); }) .catch((error) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 2d6a69fd6f5..7e9d9deda22 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -1,16 +1,16 @@ import { useState, FC } from "react"; import { useRouter } from "next/router"; -import { Button, Tooltip } from "@plane/ui"; import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks import { useWebhook, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { csvDownload } from "helpers/download.helper"; // utils import { getCurrentHookAsCSV } from "../utils"; +// ui +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; @@ -29,23 +29,21 @@ export const WebhookSecretKey: FC = observer((props) => { // store hooks const { currentWorkspace } = useWorkspace(); const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook(); - // hooks - const { setToastAlert } = useToast(); const handleCopySecretKey = () => { if (!webhookSecretKey) return; copyTextToClipboard(webhookSecretKey) .then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Secret key copied to clipboard.", }) ) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Error occurred while copying secret key.", }) @@ -59,8 +57,8 @@ export const WebhookSecretKey: FC = observer((props) => { regenerateSecretKey(workspaceSlug.toString(), data.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "New key regenerated successfully.", }); @@ -71,8 +69,8 @@ export const WebhookSecretKey: FC = observer((props) => { } }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index b4f1644698e..e8e40cf8536 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -6,9 +6,8 @@ import { Controller, useForm } from "react-hook-form"; import { WorkspaceService } from "services/workspace.service"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input } from "@plane/ui"; +import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // constants @@ -51,8 +50,6 @@ export const CreateWorkspaceForm: FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { createWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -79,8 +76,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -95,8 +92,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -104,8 +101,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { } else setSlugError(true); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }); diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index a90ac9cdf81..dbb2ef4f026 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -6,9 +6,8 @@ import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IWorkspace } from "@plane/types"; // constants @@ -32,8 +31,6 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { deleteWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -69,15 +66,15 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { element: "Workspace general settings page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/workspace/settings/invitations-list-item.tsx b/web/components/workspace/settings/invitations-list-item.tsx index 9e37a2fb51a..9a9df5cb1f5 100644 --- a/web/components/workspace/settings/invitations-list-item.tsx +++ b/web/components/workspace/settings/invitations-list-item.tsx @@ -4,11 +4,10 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, XCircle } from "lucide-react"; // hooks import { useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; @@ -30,8 +29,6 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { const { workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails }, } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // derived values const invitationDetails = getWorkspaceInvitationDetails(invitationId); @@ -40,15 +37,15 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: "Invitation removed successfully.", }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -116,8 +113,8 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 76c9bbedf8b..c6c8d1d364e 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react-lite"; import { ChevronDown, Dot, XCircle } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker"; @@ -35,8 +34,6 @@ export const WorkspaceMembersListItem: FC = observer((props) => { workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const memberDetails = getWorkspaceMemberDetails(memberId); @@ -52,8 +49,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { router.push("/profile"); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -64,8 +61,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { if (!workspaceSlug || !memberDetails) return; await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id).catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -165,8 +162,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { updateMember(workspaceSlug.toString(), memberDetails.member.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index 44da4291f66..d491ca08e45 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -7,12 +7,11 @@ import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; import { FileService } from "services/file.service"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // components import { DeleteWorkspaceModal } from "components/workspace"; import { WorkspaceImageUploadModal } from "components/core"; // ui -import { Button, CustomSelect, Input, Spinner } from "@plane/ui"; +import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -43,8 +42,6 @@ export const WorkspaceDetails: FC = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace, updateWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -77,9 +74,9 @@ export const WorkspaceDetails: FC = observer(() => { element: "Workspace general settings page", }, }); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Workspace updated successfully", }); }) @@ -110,16 +107,16 @@ export const WorkspaceDetails: FC = observer(() => { fileService.deleteFile(currentWorkspace.id, url).then(() => { updateWorkspace(currentWorkspace.slug, { logo: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace picture removed successfully.", }); setIsImageUploadModalOpen(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); @@ -132,8 +129,8 @@ export const WorkspaceDetails: FC = observer(() => { if (!currentWorkspace) return; copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Workspace URL copied to the clipboard.", }); }); diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 984bc1caf35..98a133ee356 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -9,10 +9,8 @@ import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Setting import { usePopper } from "react-popper"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Avatar, Loader } from "@plane/ui"; +import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // Static Data @@ -58,8 +56,6 @@ export const WorkspaceSidebarDropdown = observer(() => { } = useApplication(); const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); - // hooks - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); @@ -88,8 +84,8 @@ export const WorkspaceSidebarDropdown = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/components/workspace/views/delete-view-modal.tsx b/web/components/workspace/views/delete-view-modal.tsx index b6028d5417f..85d56cc6395 100644 --- a/web/components/workspace/views/delete-view-modal.tsx +++ b/web/components/workspace/views/delete-view-modal.tsx @@ -5,9 +5,8 @@ import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks import { useGlobalView, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceView } from "@plane/types"; // constants @@ -29,8 +28,6 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { // store hooks const { deleteGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -53,8 +50,8 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { view_id: data.id, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting the view. Please try again.", }); diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index b66d555fa64..6543a83212b 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -4,7 +4,8 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // store hooks import { useEventTracker, useGlobalView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { WorkspaceViewForm } from "components/workspace"; // types @@ -27,8 +28,6 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) // store hooks const { createGlobalView, updateGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -51,8 +50,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); @@ -65,8 +64,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: payload?.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be created. Please try again.", }); @@ -90,8 +89,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View updated successfully.", }); @@ -103,8 +102,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: data.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be updated. Please try again.", }); diff --git a/web/contexts/toast.context.tsx b/web/contexts/toast.context.tsx deleted file mode 100644 index 30e100b209b..00000000000 --- a/web/contexts/toast.context.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { createContext, useCallback, useReducer } from "react"; -// uuid -import { v4 as uuid } from "uuid"; -// components -import ToastAlert from "components/toast-alert"; - -export const toastContext = createContext({} as ContextType); - -// types -type ToastAlert = { - id: string; - title: string; - message?: string; - type: "success" | "error" | "warning" | "info"; -}; - -type ReducerActionType = { - type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT"; - payload: ToastAlert; -}; - -type ContextType = { - alerts?: ToastAlert[]; - removeAlert: (id: string) => void; - setToastAlert: (data: { - title: string; - type?: "success" | "error" | "warning" | "info" | undefined; - message?: string | undefined; - }) => void; -}; - -type StateType = { - toastAlerts?: ToastAlert[]; -}; - -type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; - -export const initialState: StateType = { - toastAlerts: [], -}; - -export const reducer: ReducerFunctionType = (state, action) => { - const { type, payload } = action; - - switch (type) { - case "SET_TOAST_ALERT": - return { - ...state, - toastAlerts: [...(state.toastAlerts ?? []), payload], - }; - - case "REMOVE_TOAST_ALERT": - return { - ...state, - toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id), - }; - - default: { - return state; - } - } -}; - -export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [state, dispatch] = useReducer(reducer, initialState); - - const removeAlert = useCallback((id: string) => { - dispatch({ - type: "REMOVE_TOAST_ALERT", - payload: { id, title: "", message: "", type: "success" }, - }); - }, []); - - const setToastAlert = useCallback( - (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => { - const id = uuid(); - const { title, type, message } = data; - dispatch({ - type: "SET_TOAST_ALERT", - payload: { id, title, message, type: type ?? "success" }, - }); - - const timer = setTimeout(() => { - removeAlert(id); - clearTimeout(timer); - }, 3000); - }, - [removeAlert] - ); - - return ( - - - {children} - - ); -}; diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts index 16cd8cd7992..a9aa5b91302 100644 --- a/web/helpers/theme.helper.ts +++ b/web/helpers/theme.helper.ts @@ -118,3 +118,6 @@ export const unsetCustomCssVariables = () => { dom?.style.removeProperty("--color-scheme"); } }; + +export const resolveGeneralTheme = (resolvedTheme: string | undefined) => + resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/web/hooks/use-toast.tsx b/web/hooks/use-toast.tsx deleted file mode 100644 index 6de3c104c67..00000000000 --- a/web/hooks/use-toast.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "react"; -import { toastContext } from "contexts/toast.context"; - -const useToast = () => { - const toastContextData = useContext(toastContext); - return toastContextData; -}; - -export default useToast; diff --git a/web/hooks/use-user-notifications.tsx b/web/hooks/use-user-notifications.tsx index 17a2c63dcc9..3c2ec6332a1 100644 --- a/web/hooks/use-user-notifications.tsx +++ b/web/hooks/use-user-notifications.tsx @@ -5,12 +5,12 @@ import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; // services import { NotificationService } from "services/notification.service"; -// hooks -import useToast from "./use-toast"; // fetch-keys import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; const PER_PAGE = 30; @@ -20,8 +20,6 @@ const useUserNotification = () => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const [snoozed, setSnoozed] = useState(false); const [archived, setArchived] = useState(false); const [readNotification, setReadNotification] = useState(false); @@ -265,15 +263,15 @@ const useUserNotification = () => { await userNotificationServices .markAllNotificationsAsRead(workspaceSlug.toString(), markAsReadParams) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "All Notifications marked as read.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 3e515cc6472..4d78195f139 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -7,9 +7,8 @@ import { useTheme } from "next-themes"; import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Tooltip } from "@plane/ui"; +import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -36,8 +35,6 @@ export const ProfileLayoutSidebar = observer(() => { const router = useRouter(); // next themes const { setTheme } = useTheme(); - // toast - const { setToastAlert } = useToast(); // store hooks const { theme: { sidebarCollapsed, toggleSidebar }, @@ -92,8 +89,8 @@ export const ProfileLayoutSidebar = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index 9c06947af89..a917936138d 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -3,18 +3,19 @@ import dynamic from "next/dynamic"; import Router from "next/router"; import NProgress from "nprogress"; import { observer } from "mobx-react-lite"; -import { ThemeProvider } from "next-themes"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; +// ui +import { Toast } from "@plane/ui"; // constants -import { THEMES } from "constants/themes"; +import { SWR_CONFIG } from "constants/swr-config"; // layouts import InstanceLayout from "layouts/instance-layout"; // contexts -import { ToastContextProvider } from "contexts/toast.context"; import { SWRConfig } from "swr"; -// constants -import { SWR_CONFIG } from "constants/swr-config"; +//helpers +import { resolveGeneralTheme } from "helpers/theme.helper"; // dynamic imports const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("lib/posthog-provider"), { ssr: false }); @@ -41,27 +42,29 @@ export const AppProvider: FC = observer((props) => { const { config: { envConfig }, } = useApplication(); + // themes + const { resolvedTheme } = useTheme(); return ( - - - - - - - {children} - - - - - - + <> + {/* TODO: Need to handle custom themes for toast */} + + + + + + {children} + + + + + ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index ee1be4ebb08..6e74de061d1 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -3,7 +3,6 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; import useSWR from "swr"; // hooks -import useToast from "hooks/use-toast"; import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -12,7 +11,7 @@ import { IssueDetailRoot } from "components/issues"; import { ProjectArchivedIssueDetailsHeader } from "components/headers"; import { PageHead } from "components/core"; // ui -import { ArchiveIcon, Button, Loader } from "@plane/ui"; +import { ArchiveIcon, Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { RotateCcw } from "lucide-react"; // types @@ -35,7 +34,6 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { const { issues: { restoreIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); const { membership: { currentProjectRole }, @@ -66,8 +64,8 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { await restoreIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: issue && @@ -78,8 +76,8 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index bee4fc9c724..c44f6186ef7 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -8,7 +8,6 @@ import { Controller, useForm } from "react-hook-form"; import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; -import useToast from "hooks/use-toast"; // services import { FileService } from "services/file.service"; // layouts @@ -18,7 +17,7 @@ import { GptAssistantPopover, PageHead } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; // ui import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // assets // helpers // types @@ -53,8 +52,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { currentUser, membership: { currentProjectRole }, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, @@ -148,10 +145,10 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { message: string; type: "success" | "error" | "warning" | "info"; }) => { - setToastAlert({ + setToast({ title, message, - type, + type: type as TOAST_TYPE, }); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 8c4780cba0c..1cefb941831 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -6,8 +6,8 @@ import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { PageHead } from "components/core"; @@ -22,8 +22,6 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -34,8 +32,8 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { if (!workspaceSlug || !projectId || !projectDetails) return; await updateProject(workspaceSlug.toString(), projectId.toString(), formData).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index b8739ae7756..f635588c21d 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -4,7 +4,6 @@ import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; // hooks import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -13,7 +12,7 @@ import { WorkspaceSettingHeader } from "components/headers"; import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; @@ -39,8 +38,6 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { workspace: { inviteMembersToWorkspace }, } = useMember(); const { currentWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { if (!workspaceSlug) return; @@ -59,8 +56,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "SUCCESS", element: "Workspace settings member page", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -77,8 +74,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace settings member page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: `${err.error ?? "Something went wrong. Please try again."}`, }); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index 60e65e90544..bafaa3aaad7 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -7,14 +7,12 @@ import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; // components import { WorkspaceSettingHeader } from "components/headers"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWebhook } from "@plane/types"; @@ -31,8 +29,6 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { } = useUser(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - // toast - const { setToastAlert } = useToast(); // TODO: fix this error // useEffect(() => { @@ -62,15 +58,15 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { }; await updateWebhook(workspaceSlug.toString(), formData.id, payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook updated successfully.", }); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 75023d36e9b..bc82302569b 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -1,12 +1,14 @@ import { ReactElement } from "react"; import Head from "next/head"; import { AppProps } from "next/app"; +import { ThemeProvider } from "next-themes"; // styles import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; import "styles/react-day-picker.css"; // constants +import { THEMES } from "constants/themes"; import { SITE_TITLE } from "constants/seo-variables"; // mobx store provider import { StoreProvider } from "contexts/store-context"; @@ -29,7 +31,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { {SITE_TITLE} - {getLayout()} + + {getLayout()} + ); diff --git a/web/pages/_error.tsx b/web/pages/_error.tsx index 11a7ee852e2..0a530cf9f37 100644 --- a/web/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -4,12 +4,10 @@ import { useRouter } from "next/router"; // services import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services const authService = new AuthService(); @@ -17,14 +15,12 @@ const authService = new AuthService(); const CustomErrorComponent = () => { const router = useRouter(); - const { setToastAlert } = useToast(); - const handleSignOut = async () => { await authService .signOut() .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index 0eef16009c3..e167fc0372c 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -40,8 +39,6 @@ const ForgotPasswordPage: NextPageWithLayout = () => { const { email } = router.query; // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); // form info @@ -65,8 +62,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Email sent", message: "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", @@ -77,8 +74,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index c848245ac1c..f7a49a19dbf 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -47,8 +46,6 @@ const ResetPasswordPage: NextPageWithLayout = () => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // sign in redirection hook const { handleRedirection } = useSignInRedirection(); // form info @@ -82,8 +79,8 @@ const ResetPasswordPage: NextPageWithLayout = () => { captureEvent(NEW_PASS_CREATED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index e36a1a455c3..6274fca2047 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -8,10 +8,8 @@ import { InstanceAdminLayout } from "layouts/admin-layout"; import { NextPageWithLayout } from "lib/types"; // hooks import { useApplication } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Loader, ToggleSwitch } from "@plane/ui"; +import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // components import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; import { PageHead } from "components/core"; @@ -24,9 +22,6 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); - // toast - const { setToastAlert } = useToast(); - // state const [isSubmitting, setIsSubmitting] = useState(false); @@ -46,18 +41,18 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { await updateInstanceConfigurations(payload) .then(() => { - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "SSO and OAuth Settings updated successfully", }); setIsSubmitting(false); }) .catch((err) => { console.error(err); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to update SSO and OAuth Settings", }); setIsSubmitting(false); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index b5acec19689..18441f0a0dc 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -11,12 +11,11 @@ import { WorkspaceService } from "services/workspace.service"; import { UserService } from "services/user.service"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // images import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; @@ -48,8 +47,6 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); // next-themes const { theme } = useTheme(); - // toast alert - const { setToastAlert } = useToast(); const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations()); @@ -68,8 +65,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const submitInvitations = () => { if (invitationsRespond.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one invitation.", }); @@ -101,8 +98,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { router.push(`/${redirectWorkspace?.slug}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); @@ -116,8 +113,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace invitations page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index 80e2965d67e..f37a2b6a65a 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -8,12 +8,10 @@ import { useApplication, useUser } from "hooks/store"; import { UserService } from "services/user.service"; // components import { PageHead } from "components/core"; -// hooks -import useToast from "hooks/use-toast"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; // ui -import { Button, Input, Spinner } from "@plane/ui"; +import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -46,33 +44,28 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { handleSubmit, formState: { errors, isSubmitting }, } = useForm({ defaultValues }); - const { setToastAlert } = useToast(); const handleChangePassword = async (formData: FormValues) => { if (formData.new_password !== formData.confirm_password) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "The new password and the confirm password don't match.", }); return; } - await userService - .changePassword(formData) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Password changed successfully.", - }); - }) - .catch((error) => { - setToastAlert({ - type: "error", - title: "Error!", - message: error?.error ?? "Something went wrong. Please try again.", - }); - }); + const changePasswordPromise = userService.changePassword(formData); + setPromiseToast(changePasswordPromise, { + loading: "Changing password...", + success: { + title: "Success!", + message: () => "Password changed successfully.", + }, + error: { + title: "Error!", + message: () => "Something went wrong. Please try again.", + }, + }); }; useEffect(() => { diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index c4eab324a8e..dc653cba980 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -7,14 +7,22 @@ import { FileService } from "services/file.service"; // hooks import { useApplication, useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; -import useToast from "hooks/use-toast"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components import { ImagePickerPopover, UserImageUploadModal, PageHead } from "components/core"; import { DeactivateAccountModal } from "components/account"; // ui -import { Button, CustomSelect, CustomSearchSelect, Input, Spinner } from "@plane/ui"; +import { + Button, + CustomSelect, + CustomSearchSelect, + Input, + Spinner, + TOAST_TYPE, + setPromiseToast, + setToast, +} from "@plane/ui"; // icons import { ChevronDown, User2 } from "lucide-react"; // types @@ -52,8 +60,6 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { control, formState: { errors }, } = useForm({ defaultValues }); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); // custom hooks @@ -76,24 +82,22 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { user_timezone: formData.user_timezone, }; - await updateCurrentUser(payload) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Profile updated successfully.", - }); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in updating your profile. Please try again.", - }) - ); - setTimeout(() => { - setIsLoading(false); - }, 300); + const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false)); + setPromiseToast(updateCurrentUserDetail, { + loading: "Updating...", + success: { + title: "Success!", + message: () => `Profile updated successfully.`, + }, + error: { + title: "Error!", + message: () => `There was some error in updating your profile. Please try again.`, + }, + }); + + // setTimeout(() => { + // setIsLoading(false); + // }, 300); }; const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => { @@ -105,16 +109,16 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { if (updateUser) updateCurrentUser({ avatar: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Profile picture removed successfully.", + message: "Profile picture deleted successfully.", }); setIsRemoving(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index 134ace79e99..94540aeda9c 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -3,13 +3,12 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // components import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; +import { Spinner, setPromiseToast } from "@plane/ui"; // constants import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; // type @@ -24,7 +23,6 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); useEffect(() => { if (userTheme) { @@ -37,11 +35,18 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const handleThemeChange = (themeOption: I_THEME_OPTION) => { setTheme(themeOption.value); - updateCurrentUserTheme(themeOption.value).catch(() => { - setToastAlert({ - title: "Failed to Update the theme", - type: "error", - }); + const updateCurrentUserThemePromise = updateCurrentUserTheme(themeOption.value); + + setPromiseToast(updateCurrentUserThemePromise, { + loading: "Updating theme...", + success: { + title: "Success!", + message: () => "Theme updated successfully!", + }, + error: { + title: "Error!", + message: () => "Failed to Update the theme", + }, }); }; diff --git a/web/styles/globals.css b/web/styles/globals.css index e4de1a3da81..6c51e75c47a 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -149,6 +149,27 @@ --color-onboarding-border-300: 229, 229, 229, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.1); + + /* toast theme */ + --color-toast-success-text: 62, 155, 79; + --color-toast-error-text: 220, 62, 66; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 51, 88, 212; + --color-toast-loading-text: 28, 32, 36; + --color-toast-secondary-text: 128, 131, 141; + --color-toast-tertiary-text: 96, 100, 108; + + --color-toast-success-background: 253, 253, 254; + --color-toast-error-background: 255, 252, 252; + --color-toast-warning-background: 254, 253, 251; + --color-toast-info-background: 253, 253, 254; + --color-toast-loading-background: 253, 253, 254; + + --color-toast-success-border: 218, 241, 219; + --color-toast-error-border: 255, 219, 220; + --color-toast-warning-border: 255, 247, 194; + --color-toast-info-border: 210, 222, 255; + --color-toast-loading-border: 224, 225, 230; } [data-theme="light-contrast"] { @@ -217,6 +238,27 @@ --color-onboarding-border-300: 34, 35, 38, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.1); + + /* toast theme */ + --color-toast-success-text: 178, 221, 181; + --color-toast-error-text: 206, 44, 49; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 141, 164, 239; + --color-toast-loading-text: 255, 255, 255; + --color-toast-secondary-text: 185, 187, 198; + --color-toast-tertiary-text: 139, 141, 152; + + --color-toast-success-background: 46, 46, 46; + --color-toast-error-background: 46, 46, 46; + --color-toast-warning-background: 46, 46, 46; + --color-toast-info-background: 46, 46, 46; + --color-toast-loading-background: 46, 46, 46; + + --color-toast-success-border: 42, 126, 59; + --color-toast-error-border: 100, 23, 35; + --color-toast-warning-border: 79, 52, 34; + --color-toast-info-border: 58, 91, 199; + --color-toast-loading-border: 96, 100, 108; } [data-theme="dark-contrast"] { diff --git a/yarn.lock b/yarn.lock index f413d1a44f1..81e6224e84d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8107,6 +8107,11 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +sonner@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.4.2.tgz#92740c293e9d911de726080995bd8a0cc677ccd1" + integrity sha512-x3Kfzfhb56V/ErvUnH5dZcsu6QkZpyIlRAogO4vAbN+AkBsA/8CFqOV+5djqbE5pQCpejtO4JBWL1zRj2sO/Vg== + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" From 87eadc3c5d4d6f349d95307b551bb8d8448e828e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:21:07 +0530 Subject: [PATCH 008/214] chore: issue link model field change (#3852) --- apiserver/plane/api/serializers/issue.py | 28 ++++++++++++++++++- apiserver/plane/app/serializers/issue.py | 27 ++++++++++++++++++ .../db/migrations/0061_alter_issuelink_url.py | 18 ++++++++++++ apiserver/plane/db/models/issue.py | 2 +- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 apiserver/plane/db/migrations/0061_alter_issuelink_url.py diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 4c8d6e815b1..b8f194b3273 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,8 +1,9 @@ from lxml import html - # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third party imports from rest_framework import serializers @@ -284,6 +285,20 @@ class Meta: "updated_at", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -295,6 +310,17 @@ def create(self, validated_data): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueAttachmentSerializer(BaseSerializer): class Meta: diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 411c5b73f88..1b884bedfb2 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -1,5 +1,7 @@ # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third Party imports from rest_framework import serializers @@ -432,6 +434,20 @@ class Meta: "issue", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -443,6 +459,17 @@ def create(self, validated_data): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueLinkLiteSerializer(BaseSerializer): diff --git a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py new file mode 100644 index 00000000000..1aca84a8000 --- /dev/null +++ b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-03-01 07:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0060_cycle_progress_snapshot'), + ] + + operations = [ + migrations.AlterField( + model_name='issuelink', + name='url', + field=models.TextField(), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index d5ed4247a74..5bd0b339745 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -320,7 +320,7 @@ def __str__(self): class IssueLink(ProjectBaseModel): title = models.CharField(max_length=255, null=True, blank=True) - url = models.URLField() + url = models.TextField() issue = models.ForeignKey( "db.Issue", on_delete=models.CASCADE, related_name="issue_link" ) From 126d01bdc5313a715cefb72d066efe4a5919891e Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:22:02 +0530 Subject: [PATCH 009/214] [WEB-617] fix: link behaviour fixed on formatting (#3855) * fix: link behaviour fixed on formatting * chore: added harmful script checks for links --- .../custom-link/helpers/clickHandler.ts | 14 ++- .../custom-link/helpers/pasteHandler.ts | 10 +- .../custom-link/{index.tsx => index.ts} | 97 ++++++++++++------- 3 files changed, 73 insertions(+), 48 deletions(-) rename packages/editor/core/src/ui/extensions/custom-link/{index.tsx => index.ts} (69%) diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts index 0854092a9e4..ec6c540dacc 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts @@ -15,9 +15,15 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { return false; } - const eventTarget = event.target as HTMLElement; + let a = event.target as HTMLElement; + const els = []; - if (eventTarget.nodeName !== "A") { + while (a.nodeName !== "DIV") { + els.push(a); + a = a.parentNode as HTMLElement; + } + + if (!els.find((value) => value.nodeName === "A")) { return false; } @@ -28,9 +34,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { const target = link?.target ?? attrs.target; if (link && href) { - if (view.editable) { - window.open(href, target); - } + window.open(href, target); return true; } diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts index 83e38054c74..475bf28d94b 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts @@ -33,16 +33,8 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin { return false; } - const html = event.clipboardData?.getData("text/html"); - - const hrefRegex = /href="([^"]*)"/; - - const existingLink = html?.match(hrefRegex); - - const url = existingLink ? existingLink[1] : link.href; - options.editor.commands.setMark(options.type, { - href: url, + href: link.href, }); return true; diff --git a/packages/editor/core/src/ui/extensions/custom-link/index.tsx b/packages/editor/core/src/ui/extensions/custom-link/index.ts similarity index 69% rename from packages/editor/core/src/ui/extensions/custom-link/index.tsx rename to packages/editor/core/src/ui/extensions/custom-link/index.ts index e66d18904f8..88e7abfe57c 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/index.tsx +++ b/packages/editor/core/src/ui/extensions/custom-link/index.ts @@ -1,41 +1,76 @@ -import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core"; +import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core"; import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; - -import { autolink } from "src/ui/extensions/custom-link/helpers/autolink"; -import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler"; -import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler"; +import { autolink } from "./helpers/autolink"; +import { clickHandler } from "./helpers/clickHandler"; +import { pasteHandler } from "./helpers/pasteHandler"; export interface LinkProtocolOptions { scheme: string; optionalSlashes?: boolean; } +export const pasteRegex = + /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi; + export interface LinkOptions { + /** + * If enabled, it adds links as you type. + */ autolink: boolean; - inclusive: boolean; + /** + * An array of custom protocols to be registered with linkifyjs. + */ protocols: Array; + /** + * If enabled, links will be opened on click. + */ openOnClick: boolean; + /** + * If enabled, links will be inclusive i.e. if you move your cursor to the + * link text, and start typing, it'll be a part of the link itself. + */ + inclusive: boolean; + /** + * Adds a link to the current selection if the pasted content only contains an url. + */ linkOnPaste: boolean; + /** + * A list of HTML attributes to be rendered. + */ HTMLAttributes: Record; + /** + * A validation function that modifies link verification for the auto linker. + * @param url - The url to be validated. + * @returns - True if the url is valid, false otherwise. + */ validate?: (url: string) => boolean; } declare module "@tiptap/core" { interface Commands { link: { + /** + * Set a link mark + */ setLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Toggle a link mark + */ toggleLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Unset a link mark + */ unsetLink: () => ReturnType; }; } @@ -150,37 +185,31 @@ export const CustomLinkExtension = Mark.create({ addPasteRules() { return [ markPasteRule({ - find: (text) => - find(text) - .filter((link) => { - if (this.options.validate) { - return this.options.validate(link.value); - } - return true; - }) - .filter((link) => link.isLink) - .map((link) => ({ - text: link.value, - index: link.start, - data: link, - })), - type: this.type, - getAttributes: (match, pasteEvent) => { - const html = pasteEvent?.clipboardData?.getData("text/html"); - const hrefRegex = /href="([^"]*)"/; - - const existingLink = html?.match(hrefRegex); - - if (existingLink) { - return { - href: existingLink[1], - }; + find: (text) => { + const foundLinks: PasteRuleMatch[] = []; + + if (text) { + const links = find(text).filter((item) => item.isLink); + + if (links.length) { + links.forEach((link) => + foundLinks.push({ + text: link.value, + data: { + href: link.href, + }, + index: link.start, + }) + ); + } } - return { - href: match.data?.href, - }; + return foundLinks; }, + type: this.type, + getAttributes: (match) => ({ + href: match.data?.href, + }), }), ]; }, From 5a32d10f96b2961739280e3dcee62a7cba95186e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:24:36 +0530 Subject: [PATCH 010/214] [WEB-373] chore: new dashboard updates (#3849) * chore: replaced marimekko graph with a bar graph * chore: add bar onClick handler * chore: custom date filter for widgets * style: priority graph * chore: workspace profile activity pagination * chore: profile activity pagination * chore: user profile activity pagination * chore: workspace user activity csv download * chore: download activity button added * chore: workspace user pagination * chore: collabrator pagination * chore: field change * chore: recent collaborators pagination * chore: changed the collabrators * chore: collabrators list changed * fix: distinct users * chore: search filter in collaborators * fix: import error * chore: update priority graph x-axis values * chore: admin and member request validation * chore: update csv download request method * chore: search implementation for the collaborators widget * refactor: priority distribution card * chore: add enum for duration filters * chore: update inbox types * chore: add todos for refactoring --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/urls/workspace.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/dashboard.py | 180 ++++++++------- apiserver/plane/app/views/workspace.py | 63 ++++++ packages/types/src/cycles.d.ts | 9 +- .../types/src/{ => dashboard}/dashboard.d.ts | 49 +++-- packages/types/src/dashboard/enums.ts | 8 + packages/types/src/dashboard/index.ts | 2 + packages/types/src/enums.ts | 6 + .../{inbox.d.ts => inbox/inbox-types.d.ts} | 32 +-- packages/types/src/inbox/root.d.ts | 3 +- packages/types/src/index.d.ts | 4 +- packages/types/src/modules.d.ts | 28 +-- packages/types/src/users.d.ts | 26 +-- .../core/filters/date-filter-select.tsx | 11 +- .../dashboard/widgets/assigned-issues.tsx | 23 +- .../dashboard/widgets/created-issues.tsx | 23 +- .../widgets/dropdowns/duration-filter.tsx | 56 +++-- .../widgets/issue-panels/tabs-list.tsx | 6 +- .../dashboard/widgets/issues-by-priority.tsx | 149 +++---------- .../widgets/issues-by-state-group.tsx | 21 +- .../widgets/loaders/recent-collaborators.tsx | 15 +- .../dashboard/widgets/recent-activity.tsx | 15 +- .../widgets/recent-collaborators.tsx | 94 -------- .../collaborators-list.tsx | 120 ++++++++++ .../recent-collaborators/default-list.tsx | 59 +++++ .../widgets/recent-collaborators/index.ts | 1 + .../widgets/recent-collaborators/root.tsx | 48 ++++ .../recent-collaborators/search-list.tsx | 80 +++++++ web/components/graphs/index.ts | 1 + web/components/graphs/issues-by-priority.tsx | 103 +++++++++ web/components/inbox/inbox-issue-actions.tsx | 4 +- .../profile/activity/activity-list.tsx | 162 ++++++++++++++ .../profile/activity/download-button.tsx | 57 +++++ web/components/profile/activity/index.ts | 4 + .../activity/profile-activity-list.tsx | 190 ++++++++++++++++ .../activity/workspace-activity-list.tsx | 50 +++++ web/components/profile/index.ts | 1 + web/components/profile/navbar.tsx | 9 +- web/components/profile/overview/activity.tsx | 9 +- .../overview/priority-distribution.tsx | 88 -------- .../overview/priority-distribution/index.ts | 1 + .../priority-distribution/main-content.tsx | 31 +++ .../priority-distribution.tsx | 33 +++ .../profile/overview/state-distribution.tsx | 4 +- web/components/profile/overview/workload.tsx | 6 +- web/components/profile/sidebar.tsx | 4 +- web/components/ui/graphs/index.ts | 1 - web/components/ui/graphs/marimekko-graph.tsx | 48 ---- web/constants/dashboard.ts | 20 +- web/constants/fetch-keys.ts | 11 +- web/constants/profile.ts | 5 + web/helpers/dashboard.helper.ts | 40 +++- web/layouts/user-profile-layout/layout.tsx | 16 +- web/package.json | 1 - .../profile/[userId]/activity.tsx | 84 +++++++ .../profile/[userId]/index.tsx | 2 +- web/pages/profile/activity.tsx | 207 ++++-------------- web/services/user.service.ts | 44 ++-- web/store/user/index.ts | 18 -- yarn.lock | 13 -- 61 files changed, 1564 insertions(+), 841 deletions(-) rename packages/types/src/{ => dashboard}/dashboard.d.ts (79%) create mode 100644 packages/types/src/dashboard/enums.ts create mode 100644 packages/types/src/dashboard/index.ts create mode 100644 packages/types/src/enums.ts rename packages/types/src/{inbox.d.ts => inbox/inbox-types.d.ts} (65%) delete mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/default-list.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/index.ts create mode 100644 web/components/dashboard/widgets/recent-collaborators/root.tsx create mode 100644 web/components/dashboard/widgets/recent-collaborators/search-list.tsx create mode 100644 web/components/graphs/index.ts create mode 100644 web/components/graphs/issues-by-priority.tsx create mode 100644 web/components/profile/activity/activity-list.tsx create mode 100644 web/components/profile/activity/download-button.tsx create mode 100644 web/components/profile/activity/index.ts create mode 100644 web/components/profile/activity/profile-activity-list.tsx create mode 100644 web/components/profile/activity/workspace-activity-list.tsx delete mode 100644 web/components/profile/overview/priority-distribution.tsx create mode 100644 web/components/profile/overview/priority-distribution/index.ts create mode 100644 web/components/profile/overview/priority-distribution/main-content.tsx create mode 100644 web/components/profile/overview/priority-distribution/priority-distribution.tsx delete mode 100644 web/components/ui/graphs/marimekko-graph.tsx create mode 100644 web/pages/[workspaceSlug]/profile/[userId]/activity.tsx diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index a70ff18e535..8b21bb9e1b2 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -22,6 +22,7 @@ WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, ) @@ -191,6 +192,11 @@ WorkspaceUserActivityEndpoint.as_view(), name="workspace-user-activity", ), + path( + "workspaces//user-activity//export/", + ExportWorkspaceUserActivityEndpoint.as_view(), + name="export-workspace-user-activity", + ), path( "workspaces//user-profile//", WorkspaceUserProfileEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index d4a13e49749..6af60ff9c18 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -49,6 +49,7 @@ WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, ) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard.py index 62ce0d910fe..9078d2ab548 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard.py @@ -14,6 +14,7 @@ JSONField, Func, Prefetch, + IntegerField, ) from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField @@ -38,6 +39,8 @@ IssueLink, IssueAttachment, IssueRelation, + IssueAssignee, + User, ) from plane.app.serializers import ( IssueActivitySerializer, @@ -212,11 +215,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -231,11 +234,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -365,11 +368,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -382,11 +385,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -503,7 +506,9 @@ def dashboard_recent_projects(self, request, slug): ).exclude(id__in=unique_project_ids) # Append additional project IDs to the existing list - unique_project_ids.update(additional_projects.values_list("id", flat=True)) + unique_project_ids.update( + additional_projects.values_list("id", flat=True) + ) return Response( list(unique_project_ids)[:4], @@ -512,90 +517,97 @@ def dashboard_recent_projects(self, request, slug): def dashboard_recent_collaborators(self, request, slug): - # Fetch all project IDs where the user belongs to - user_projects = Project.objects.filter( - project_projectmember__member=request.user, - project_projectmember__is_active=True, - workspace__slug=slug, - ).values_list("id", flat=True) - - # Fetch all users who have performed an activity in the projects where the user exists - users_with_activities = ( + # Subquery to count activities for each project member + activity_count_subquery = ( IssueActivity.objects.filter( workspace__slug=slug, - project_id__in=user_projects, + actor=OuterRef("member"), + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .values("actor") - .exclude(actor=request.user) - .annotate(num_activities=Count("actor")) - .order_by("-num_activities") - )[:7] - - # Get the count of active issues for each user in users_with_activities - users_with_active_issues = [] - for user_activity in users_with_activities: - user_id = user_activity["actor"] - active_issue_count = Issue.objects.filter( - assignees__in=[user_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - {"user_id": user_id, "active_issue_count": active_issue_count} + .annotate(num_activities=Count("pk")) + .values("num_activities") + ) + + # Get all project members and annotate them with activity counts + project_members_with_activities = ( + ProjectMember.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .annotate( + num_activities=Coalesce( + Subquery(activity_count_subquery), + Value(0), + output_field=IntegerField(), + ), + is_current_user=Case( + When(member=request.user, then=Value(0)), + default=Value(1), + output_field=IntegerField(), + ), + ) + .values_list("member", flat=True) + .order_by("is_current_user", "-num_activities") + .distinct() + ) + search = request.query_params.get("search", None) + if search: + project_members_with_activities = ( + project_members_with_activities.filter( + Q(member__display_name__icontains=search) + | Q(member__first_name__icontains=search) + | Q(member__last_name__icontains=search) + ) ) - # Insert the logged-in user's ID and their active issue count at the beginning - active_issue_count = Issue.objects.filter( - assignees__in=[request.user], - state__group__in=["unstarted", "started"], - ).count() + return self.paginate( + request=request, + queryset=project_members_with_activities, + controller=self.get_results_controller, + ) - if users_with_activities.count() < 7: - # Calculate the additional collaborators needed - additional_collaborators_needed = 7 - users_with_activities.count() - - # Fetch additional collaborators from the project_member table - additional_collaborators = list( - set( - ProjectMember.objects.filter( - ~Q(member=request.user), - project_id__in=user_projects, - workspace__slug=slug, - ) - .exclude( - member__in=[ - user["actor"] for user in users_with_activities - ] + +class DashboardEndpoint(BaseAPIView): + def get_results_controller(self, project_members_with_activities): + user_active_issue_counts = ( + User.objects.filter(id__in=project_members_with_activities) + .annotate( + active_issue_count=Count( + Case( + When( + issue_assignee__issue__state__group__in=[ + "unstarted", + "started", + ], + then=1, + ), + output_field=IntegerField(), + ) ) - .values_list("member", flat=True) ) + .values("active_issue_count", user_id=F("id")) ) + # Create a dictionary to store the active issue counts by user ID + active_issue_counts_dict = { + user["user_id"]: user["active_issue_count"] + for user in user_active_issue_counts + } - additional_collaborators = additional_collaborators[ - :additional_collaborators_needed + # Preserve the sequence of project members with activities + paginated_results = [ + { + "user_id": member_id, + "active_issue_count": active_issue_counts_dict.get( + member_id, 0 + ), + } + for member_id in project_members_with_activities ] + return paginated_results - # Append additional collaborators to the list - for collaborator_id in additional_collaborators: - active_issue_count = Issue.objects.filter( - assignees__in=[collaborator_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - { - "user_id": str(collaborator_id), - "active_issue_count": active_issue_count, - } - ) - - users_with_active_issues.insert( - 0, - {"user_id": request.user.id, "active_issue_count": active_issue_count}, - ) - - return Response(users_with_active_issues, status=status.HTTP_200_OK) - - -class DashboardEndpoint(BaseAPIView): def create(self, request, slug): serializer = DashboardSerializer(data=request.data) if serializer.is_valid(): @@ -622,7 +634,9 @@ def get(self, request, slug, dashboard_id=None): dashboard_type = request.GET.get("dashboard_type", None) if dashboard_type == "home": dashboard, created = Dashboard.objects.get_or_create( - type_identifier=dashboard_type, owned_by=request.user, is_default=True + type_identifier=dashboard_type, + owned_by=request.user, + is_default=True, ) if created: @@ -639,7 +653,9 @@ def get(self, request, slug, dashboard_id=None): updated_dashboard_widgets = [] for widget_key in widgets_to_fetch: - widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True) + widget = Widget.objects.filter( + key=widget_key + ).values_list("id", flat=True) if widget: updated_dashboard_widgets.append( DashboardWidget( diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 47de86a1c17..84ba125bac1 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1,9 +1,12 @@ # Python imports import jwt +import csv +import io from datetime import date, datetime from dateutil.relativedelta import relativedelta # Django imports +from django.http import HttpResponse from django.db import IntegrityError from django.conf import settings from django.utils import timezone @@ -1238,6 +1241,66 @@ def get(self, request, slug, user_id): ) +class ExportWorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def generate_csv_from_rows(self, rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + csv_buffer.seek(0) + return csv_buffer + + def post(self, request, slug, user_id): + + if not request.data.get("date"): + return Response( + {"error": "Date is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user_activities = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + created_at__date=request.data.get("date"), + project__project_projectmember__member=request.user, + actor_id=user_id, + ).select_related("actor", "workspace", "issue", "project")[:10000] + + header = [ + "Actor name", + "Issue ID", + "Project", + "Created at", + "Updated at", + "Action", + "Field", + "Old value", + "New value", + ] + rows = [ + ( + activity.actor.display_name, + f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", + activity.project.name, + activity.created_at, + activity.updated_at, + activity.verb, + activity.field, + activity.old_value, + activity.new_value, + ) + for activity in user_activities + ] + csv_buffer = self.generate_csv_from_rows([header] + rows) + response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"' + return response + + class WorkspaceUserProfileEndpoint(BaseAPIView): def get(self, request, slug, user_id): user_data = User.objects.get(pk=user_id) diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index e7ec66ae212..25b7427f519 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -1,11 +1,4 @@ -import type { - IUser, - TIssue, - IProjectLite, - IWorkspaceLite, - IIssueFilterOptions, - IUserLite, -} from "@plane/types"; +import type { TIssue, IIssueFilterOptions } from "@plane/types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard/dashboard.d.ts similarity index 79% rename from packages/types/src/dashboard.d.ts rename to packages/types/src/dashboard/dashboard.d.ts index 407b5cd794e..d565f668867 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard/dashboard.d.ts @@ -1,7 +1,8 @@ -import { IIssueActivity, TIssuePriorities } from "./issues"; -import { TIssue } from "./issues/issue"; -import { TIssueRelationTypes } from "./issues/issue_relation"; -import { TStateGroups } from "./state"; +import { IIssueActivity, TIssuePriorities } from "../issues"; +import { TIssue } from "../issues/issue"; +import { TIssueRelationTypes } from "../issues/issue_relation"; +import { TStateGroups } from "../state"; +import { EDurationFilters } from "./enums"; export type TWidgetKeys = | "overview_stats" @@ -15,30 +16,27 @@ export type TWidgetKeys = export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; -export type TDurationFilterOptions = - | "none" - | "today" - | "this_week" - | "this_month" - | "this_year"; - // widget filters export type TAssignedIssuesWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; tab?: TIssuesListTypes; }; export type TCreatedIssuesWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; tab?: TIssuesListTypes; }; export type TIssuesByStateGroupsWidgetFilters = { - duration?: TDurationFilterOptions; + duration?: EDurationFilters; + custom_dates?: string[]; }; export type TIssuesByPriorityWidgetFilters = { - duration?: TDurationFilterOptions; + custom_dates?: string[]; + duration?: EDurationFilters; }; export type TWidgetFiltersFormData = @@ -97,6 +95,12 @@ export type TWidgetStatsRequestParams = | { target_date: string; widget_key: "issues_by_priority"; + } + | { + cursor: string; + per_page: number; + search?: string; + widget_key: "recent_collaborators"; }; export type TWidgetIssue = TIssue & { @@ -141,8 +145,17 @@ export type TRecentActivityWidgetResponse = IIssueActivity; export type TRecentProjectsWidgetResponse = string[]; export type TRecentCollaboratorsWidgetResponse = { - active_issue_count: number; - user_id: string; + count: number; + extra_stats: Object | null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: { + active_issue_count: number; + user_id: string; + }[]; + total_pages: number; }; export type TWidgetStatsResponse = @@ -153,7 +166,7 @@ export type TWidgetStatsResponse = | TCreatedIssuesWidgetResponse | TRecentActivityWidgetResponse[] | TRecentProjectsWidgetResponse - | TRecentCollaboratorsWidgetResponse[]; + | TRecentCollaboratorsWidgetResponse; // dashboard export type TDashboard = { diff --git a/packages/types/src/dashboard/enums.ts b/packages/types/src/dashboard/enums.ts new file mode 100644 index 00000000000..2c9efd5c35d --- /dev/null +++ b/packages/types/src/dashboard/enums.ts @@ -0,0 +1,8 @@ +export enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} diff --git a/packages/types/src/dashboard/index.ts b/packages/types/src/dashboard/index.ts new file mode 100644 index 00000000000..dec14aea6a9 --- /dev/null +++ b/packages/types/src/dashboard/index.ts @@ -0,0 +1,2 @@ +export * from "./dashboard"; +export * from "./enums"; diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts new file mode 100644 index 00000000000..259f13e9bc2 --- /dev/null +++ b/packages/types/src/enums.ts @@ -0,0 +1,6 @@ +export enum EUserProjectRoles { + GUEST = 5, + VIEWER = 10, + MEMBER = 15, + ADMIN = 20, +} diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox/inbox-types.d.ts similarity index 65% rename from packages/types/src/inbox.d.ts rename to packages/types/src/inbox/inbox-types.d.ts index 4d666ae8356..9db71c3ee4c 100644 --- a/packages/types/src/inbox.d.ts +++ b/packages/types/src/inbox/inbox-types.d.ts @@ -1,5 +1,5 @@ -import { TIssue } from "./issues/base"; -import type { IProjectLite } from "./projects"; +import { TIssue } from "../issues/base"; +import type { IProjectLite } from "../projects"; export type TInboxIssueExtended = { completed_at: string | null; @@ -33,34 +33,6 @@ export interface IInbox { workspace: string; } -interface StatePending { - readonly status: -2; -} -interface StatusReject { - status: -1; -} - -interface StatusSnoozed { - status: 0; - snoozed_till: Date; -} - -interface StatusAccepted { - status: 1; -} - -interface StatusDuplicate { - status: 2; - duplicate_to: string; -} - -export type TInboxStatus = - | StatusReject - | StatusSnoozed - | StatusAccepted - | StatusDuplicate - | StatePending; - export interface IInboxFilterOptions { priority?: string[] | null; inbox_status?: number[] | null; diff --git a/packages/types/src/inbox/root.d.ts b/packages/types/src/inbox/root.d.ts index 2f10c088def..6fd21a4fe9e 100644 --- a/packages/types/src/inbox/root.d.ts +++ b/packages/types/src/inbox/root.d.ts @@ -1,2 +1,3 @@ -export * from "./inbox"; export * from "./inbox-issue"; +export * from "./inbox-types"; +export * from "./inbox"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 6e8ded94296..b1eb38a567f 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -4,7 +4,6 @@ export * from "./cycles"; export * from "./dashboard"; export * from "./projects"; export * from "./state"; -export * from "./invitation"; export * from "./issues"; export * from "./modules"; export * from "./views"; @@ -15,7 +14,6 @@ export * from "./estimate"; export * from "./importer"; // FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable -export * from "./inbox"; export * from "./inbox/root"; export * from "./analytics"; @@ -32,6 +30,8 @@ export * from "./api_token"; export * from "./instance"; export * from "./app"; +export * from "./enums"; + export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object ? ObjectType[Key] extends { pop: any; push: any } diff --git a/packages/types/src/modules.d.ts b/packages/types/src/modules.d.ts index fcf2d86a21a..c532a467c7e 100644 --- a/packages/types/src/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -1,16 +1,12 @@ -import type { - IUser, - IUserLite, - TIssue, - IProject, - IWorkspace, - IWorkspaceLite, - IProjectLite, - IIssueFilterOptions, - ILinkDetails, -} from "@plane/types"; +import type { TIssue, IIssueFilterOptions, ILinkDetails } from "@plane/types"; -export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; +export type TModuleStatus = + | "backlog" + | "planned" + | "in-progress" + | "paused" + | "completed" + | "cancelled"; export interface IModule { backlog_issues: number; @@ -68,6 +64,10 @@ export type ModuleLink = { url: string; }; -export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; +export type SelectModuleType = + | (IModule & { actionType: "edit" | "delete" | "create-issue" }) + | undefined; -export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined; +export type SelectIssue = + | (TIssue & { actionType: "edit" | "delete" | "create" }) + | undefined; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 81c8abcd5f0..c428dc7d284 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,5 +1,9 @@ -import { EUserProjectRoles } from "constants/project"; -import { IIssueActivity, IIssueLite, TStateGroups } from "."; +import { + IIssueActivity, + TIssuePriorities, + TStateGroups, + EUserProjectRoles, +} from "."; export interface IUser { id: string; @@ -17,7 +21,6 @@ export interface IUser { is_onboarded: boolean; is_password_autoset: boolean; is_tour_completed: boolean; - is_password_autoset: boolean; mobile_number: string | null; role: string | null; onboarding_step: { @@ -80,7 +83,7 @@ export interface IUserActivity { } export interface IUserPriorityDistribution { - priority: string; + priority: TIssuePriorities; priority_count: number; } @@ -89,21 +92,6 @@ export interface IUserStateDistribution { state_count: number; } -export interface IUserWorkspaceDashboard { - assigned_issues_count: number; - completed_issues_count: number; - issue_activities: IUserActivity[]; - issues_due_week_count: number; - overdue_issues: IIssueLite[]; - completed_issues: { - week_in_month: number; - completed_count: number; - }[]; - pending_issues_count: number; - state_distribution: IUserStateDistribution[]; - upcoming_issues: IIssueLite[]; -} - export interface IUserActivityResponse { count: number; extra_stats: null; diff --git a/web/components/core/filters/date-filter-select.tsx b/web/components/core/filters/date-filter-select.tsx index 9bb10f800d0..47207e0ccd8 100644 --- a/web/components/core/filters/date-filter-select.tsx +++ b/web/components/core/filters/date-filter-select.tsx @@ -1,10 +1,7 @@ import React from "react"; - +import { CalendarDays } from "lucide-react"; // ui import { CustomSelect, CalendarAfterIcon, CalendarBeforeIcon } from "@plane/ui"; -// icons -import { CalendarDays } from "lucide-react"; -// fetch-keys type Props = { title: string; @@ -22,17 +19,17 @@ const dueDateRange: DueDate[] = [ { name: "before", value: "before", - icon: , + icon: , }, { name: "after", value: "after", - icon: , + icon: , }, { name: "range", value: "range", - icon: , + icon: , }, ]; diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 7c8fbd2a98e..407ac9ddf24 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -15,7 +15,7 @@ import { // helpers import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; @@ -30,8 +30,9 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -53,7 +57,7 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -81,8 +85,17 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { Assigned to you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index e7832883bc9..23e7bee2771 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -15,7 +15,7 @@ import { // helpers import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; @@ -30,8 +30,9 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -52,7 +56,7 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -78,8 +82,17 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { Created by you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index 4844ea406e5..fbdac4f00e0 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -1,36 +1,58 @@ +import { useState } from "react"; import { ChevronDown } from "lucide-react"; +// components +import { DateFilterModal } from "components/core"; // ui import { CustomMenu } from "@plane/ui"; +// helpers +import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; // types -import { TDurationFilterOptions } from "@plane/types"; +import { EDurationFilters } from "@plane/types"; // constants import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; type Props = { - onChange: (value: TDurationFilterOptions) => void; - value: TDurationFilterOptions; + customDates?: string[]; + onChange: (value: EDurationFilters, customDates?: string[]) => void; + value: EDurationFilters; }; export const DurationFilterDropdown: React.FC = (props) => { - const { onChange, value } = props; + const { customDates, onChange, value } = props; + // states + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); return ( - - {DURATION_FILTER_OPTIONS.find((option) => option.key === value)?.label} - -
    - } - placement="bottom-end" - closeOnSelect - > + <> + setIsDateFilterModalOpen(false)} + onSelect={(val) => onChange(EDurationFilters.CUSTOM, val)} + title="Due date" + /> + + {getDurationFilterDropdownLabel(value, customDates ?? [])} + +
    + } + placement="bottom-end" + closeOnSelect + > {DURATION_FILTER_OPTIONS.map((option) => ( - onChange(option.key)}> + { + if (option.key === "custom") setIsDateFilterModalOpen(true); + else onChange(option.key); + }} + > {option.label} ))} - + + ); }; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index 306c2fdeb9a..d18f08f2755 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -3,12 +3,12 @@ import { Tab } from "@headlessui/react"; // helpers import { cn } from "helpers/common.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes } from "@plane/types"; // constants import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { - durationFilter: TDurationFilterOptions; + durationFilter: EDurationFilters; selectedTab: TIssuesListTypes; }; @@ -48,7 +48,7 @@ export const TabsList: React.FC = observer((props) => { className={cn( "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", { - "text-custom-text-100 bg-custom-background-100": selectedTab === tab.key, + "text-custom-text-100": selectedTab === tab.key, "hover:text-custom-text-300": selectedTab !== tab.key, } )} diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 91e321b05f7..3e9823fe4e9 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,82 +1,36 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks import { useDashboard } from "hooks/store"; // components -import { MarimekkoGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByPriorityEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// ui -import { PriorityIcon } from "@plane/ui"; // helpers import { getCustomDates } from "helpers/dashboard.helper"; // types -import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants -import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; -import { ISSUE_PRIORITIES } from "constants/issue"; - -const TEXT_COLORS = { - urgent: "#F4A9AA", - high: "#AB4800", - medium: "#AB6400", - low: "#1F2D5C", - none: "#60646C", -}; - -const CustomBar = (props: any) => { - const { bar, workspaceSlug } = props; - // states - const [isMouseOver, setIsMouseOver] = useState(false); - - return ( - - setIsMouseOver(true)} - onMouseLeave={() => setIsMouseOver(false)} - > - - - {bar?.id} - - - - ); -}; +import { IssuesByPriorityGraph } from "components/graphs"; const WIDGET_KEY = "issues_by_priority"; export const IssuesByPriorityWidget: React.FC = observer((props) => { const { dashboardId, workspaceSlug } = props; + // router + const router = useRouter(); // store hooks const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -86,7 +40,10 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -94,7 +51,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => }; useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -105,31 +62,10 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => if (!widgetDetails || !widgetStats) return ; const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats - .filter((i) => i.count !== 0) - .map((item) => ({ - priority: item?.priority, - percentage: (item?.count / totalCount) * 100, - urgent: item?.priority === "urgent" ? 1 : 0, - high: item?.priority === "high" ? 1 : 0, - medium: item?.priority === "medium" ? 1 : 0, - low: item?.priority === "low" ? 1 : 0, - none: item?.priority === "none" ? 1 : 0, - })); - - const CustomBarsLayer = (props: any) => { - const { bars } = props; - - return ( - - {bars - ?.filter((b: any) => b?.value === 1) // render only bars with value 1 - .map((bar: any) => ( - - ))} - - ); - }; + const chartData = widgetStats.map((item) => ({ + priority: item?.priority, + priority_count: item?.count, + })); return (
    @@ -141,60 +77,27 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => Assigned by priority + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } />
    {totalCount > 0 ? ( -
    +
    - ({ - id: p.key, - value: p.key, - }))} - axisBottom={null} - axisLeft={null} - height="119px" - margin={{ - top: 11, - right: 0, - bottom: 0, - left: 0, + onBarClick={(datum) => { + router.push( + `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` + ); }} - defs={PRIORITY_GRAPH_GRADIENTS} - fill={ISSUE_PRIORITIES.map((p) => ({ - match: { - id: p.key, - }, - id: `gradient${p.title}`, - }))} - tooltip={() => <>} - enableGridX={false} - enableGridY={false} - layers={[CustomBarsLayer]} /> -
    - {chartData.map((item) => ( -

    - - {item.percentage.toFixed(0)}% -

    - ))} -
    ) : ( diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index a0eb6c70f8c..b301d30f3fe 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -15,7 +15,12 @@ import { // helpers import { getCustomDates } from "helpers/dashboard.helper"; // types -import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; +import { + EDurationFilters, + TIssuesByStateGroupsWidgetFilters, + TIssuesByStateGroupsWidgetResponse, + TStateGroups, +} from "@plane/types"; // constants import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; import { STATE_GROUPS } from "constants/state"; @@ -34,7 +39,8 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -44,7 +50,10 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -53,7 +62,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // fetch widget stats useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -139,10 +148,12 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) Assigned by state + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } /> diff --git a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx index d838967af76..dc2163128fc 100644 --- a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx +++ b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx @@ -2,17 +2,16 @@ import { Loader } from "@plane/ui"; export const RecentCollaboratorsWidgetLoader = () => ( - - -
    - {Array.from({ length: 8 }).map((_, index) => ( -
    + <> + {Array.from({ length: 8 }).map((_, index) => ( + +
    - ))} -
    - + + ))} + ); diff --git a/web/components/dashboard/widgets/recent-activity.tsx b/web/components/dashboard/widgets/recent-activity.tsx index fc16946d8e1..ec99ca771a2 100644 --- a/web/components/dashboard/widgets/recent-activity.tsx +++ b/web/components/dashboard/widgets/recent-activity.tsx @@ -8,11 +8,12 @@ import { useDashboard, useUser } from "hooks/store"; import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; // ui -import { Avatar } from "@plane/ui"; +import { Avatar, getButtonStyling } from "@plane/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; // types import { TRecentActivityWidgetResponse } from "@plane/types"; +import { cn } from "helpers/common.helper"; const WIDGET_KEY = "recent_activity"; @@ -23,6 +24,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { // derived values const { fetchWidgetStats, getWidgetStats } = useDashboard(); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; useEffect(() => { fetchWidgetStats(workspaceSlug, dashboardId, { @@ -35,7 +37,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { return (
    - + Your issue activities {widgetStats.length > 0 ? ( @@ -83,6 +85,15 @@ export const RecentActivityWidget: React.FC = observer((props) => {
    ))} + + View all +
    ) : (
    diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx deleted file mode 100644 index 2fafbb9acaf..00000000000 --- a/web/components/dashboard/widgets/recent-collaborators.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from "react"; -import Link from "next/link"; -import { observer } from "mobx-react-lite"; -// hooks -import { useDashboard, useMember, useUser } from "hooks/store"; -// components -import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; -// ui -import { Avatar } from "@plane/ui"; -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails) return null; - - return ( - -
    - -
    -
    - {isCurrentUser ? "You" : userDetails?.display_name} -
    -

    - {issueCount} active issue{issueCount > 1 ? "s" : ""} -

    - - ); -}); - -export const RecentCollaboratorsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
    -
    -

    Most active members

    -

    - Top eight active members in your project by last activity -

    -
    - {widgetStats.length > 1 ? ( -
    - {widgetStats.map((user) => ( - - ))} -
    - ) : ( -
    - -
    - )} -
    - ); -}); diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx new file mode 100644 index 00000000000..48c44807535 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -0,0 +1,120 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// store hooks +import { useDashboard, useMember, useUser } from "hooks/store"; +// components +import { WidgetLoader } from "../loaders"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails) return null; + + return ( + +
    + +
    +
    + {isCurrentUser ? "You" : userDetails?.display_name} +
    +

    + {issueCount} active issue{issueCount > 1 ? "s" : ""} +

    + + ); +}); + +type CollaboratorsListProps = { + cursor: string; + dashboardId: string; + perPage: number; + searchQuery?: string; + updateIsLoading?: (isLoading: boolean) => void; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +export const CollaboratorsList: React.FC = (props) => { + const { + cursor, + dashboardId, + perPage, + searchQuery = "", + updateIsLoading, + updateResultsCount, + updateTotalPages, + workspaceSlug, + } = props; + // store hooks + const { fetchWidgetStats } = useDashboard(); + + const { data: widgetStats } = useSWR( + workspaceSlug && dashboardId && cursor + ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}_${cursor}_${searchQuery}` + : null, + workspaceSlug && dashboardId && cursor + ? () => + fetchWidgetStats(workspaceSlug, dashboardId, { + cursor, + per_page: perPage, + search: searchQuery, + widget_key: WIDGET_KEY, + }) + : null + ) as { + data: TRecentCollaboratorsWidgetResponse | undefined; + }; + + useEffect(() => { + updateIsLoading?.(true); + + if (!widgetStats) return; + + updateIsLoading?.(false); + updateTotalPages(widgetStats.total_pages); + updateResultsCount(widgetStats.results.length); + }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); + + if (!widgetStats) return ; + + return ( + <> + {widgetStats?.results.map((user) => ( + + ))} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx new file mode 100644 index 00000000000..d3f85782434 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +// components +import { CollaboratorsList } from "./collaborators-list"; +// ui +import { Button } from "@plane/ui"; + +type Props = { + dashboardId: string; + perPage: number; + workspaceSlug: string; +}; + +export const DefaultCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + return ( + <> +
    + {collaboratorsPages} +
    + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/index.ts b/web/components/dashboard/widgets/recent-collaborators/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx new file mode 100644 index 00000000000..5f611b46243 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { Search } from "lucide-react"; +// components +import { DefaultCollaboratorsList } from "./default-list"; +import { SearchedCollaboratorsList } from "./search-list"; +8; +// types +import { WidgetProps } from "components/dashboard/widgets"; + +const PER_PAGE = 8; + +export const RecentCollaboratorsWidget: React.FC = (props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [searchQuery, setSearchQuery] = useState(""); + + return ( +
    +
    +
    +

    Most active members

    +

    + Top eight active members in your project by last activity +

    +
    +
    + + setSearchQuery(e.target.value)} + /> +
    +
    + {searchQuery.trim() !== "" ? ( + + ) : ( + + )} +
    + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/search-list.tsx b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx new file mode 100644 index 00000000000..cd882610049 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +// components +import { CollaboratorsList } from "./collaborators-list"; +// ui +import { Button } from "@plane/ui"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/recent-collaborators-1.svg"; +import LightImage from "public/empty-state/dashboard/light/recent-collaborators-1.svg"; + +type Props = { + dashboardId: string; + perPage: number; + searchQuery: string; + workspaceSlug: string; +}; + +export const SearchedCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, searchQuery, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + // next-themes + const { resolvedTheme } = useTheme(); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( + <> +
    + {collaboratorsPages} +
    + {!isLoading && totalPages === 0 && ( +
    +
    + Recent collaborators +
    +

    No matching member

    +
    + )} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} + + ); +}; diff --git a/web/components/graphs/index.ts b/web/components/graphs/index.ts new file mode 100644 index 00000000000..305c3944ea4 --- /dev/null +++ b/web/components/graphs/index.ts @@ -0,0 +1 @@ +export * from "./issues-by-priority"; diff --git a/web/components/graphs/issues-by-priority.tsx b/web/components/graphs/issues-by-priority.tsx new file mode 100644 index 00000000000..0d4bf37b5fe --- /dev/null +++ b/web/components/graphs/issues-by-priority.tsx @@ -0,0 +1,103 @@ +import { Theme } from "@nivo/core"; +import { ComputedDatum } from "@nivo/bar"; +// components +import { BarGraph } from "components/ui"; +// helpers +import { capitalizeFirstLetter } from "helpers/string.helper"; +// types +import { TIssuePriorities } from "@plane/types"; +// constants +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; + +type Props = { + borderRadius?: number; + data: { + priority: TIssuePriorities; + priority_count: number; + }[]; + height?: number; + onBarClick?: ( + datum: ComputedDatum & { + color: string; + } + ) => void; + padding?: number; + theme?: Theme; +}; + +const PRIORITY_TEXT_COLORS = { + urgent: "#CE2C31", + high: "#AB4800", + medium: "#AB6400", + low: "#1F2D5C", + none: "#60646C", +}; + +export const IssuesByPriorityGraph: React.FC = (props) => { + const { borderRadius = 8, data, height = 300, onBarClick, padding = 0.05, theme } = props; + + const chartData = data.map((priority) => ({ + priority: capitalizeFirstLetter(priority.priority), + value: priority.priority_count, + })); + + return ( + p.priority_count)} + axisBottom={{ + tickPadding: 8, + tickSize: 0, + }} + tooltip={(datum) => ( +
    + + {datum.data.priority}: + {datum.value} +
    + )} + colors={({ data }) => `url(#gradient${data.priority})`} + defs={PRIORITY_GRAPH_GRADIENTS} + fill={ISSUE_PRIORITIES.map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.title}`, + }))} + onClick={(datum) => { + if (onBarClick) onBarClick(datum); + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + ticks: { + text: { + fontSize: 13, + }, + }, + }, + grid: { + line: { + stroke: "transparent", + }, + }, + ...theme, + }} + /> + ); +}; diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 200d541abae..4d4cfa0cca6 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -17,7 +17,7 @@ import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; // types -import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; +import type { TInboxDetailedStatus } from "@plane/types"; import { EUserProjectRoles } from "constants/project"; import { ISSUE_DELETED } from "constants/event-tracker"; @@ -29,7 +29,7 @@ type TInboxIssueActionsHeader = { }; type TInboxIssueOperations = { - updateInboxIssueStatus: (data: TInboxStatus) => Promise; + updateInboxIssueStatus: (data: TInboxDetailedStatus) => Promise; removeInboxIssue: () => Promise; }; diff --git a/web/components/profile/activity/activity-list.tsx b/web/components/profile/activity/activity-list.tsx new file mode 100644 index 00000000000..06691272104 --- /dev/null +++ b/web/components/profile/activity/activity-list.tsx @@ -0,0 +1,162 @@ +import Link from "next/link"; +import { observer } from "mobx-react"; +import { History, MessageSquare } from "lucide-react"; +// editor +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +// hooks +import { useUser } from "hooks/store"; +// components +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +// ui +import { ActivitySettingsLoader } from "components/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +// types +import { IUserActivityResponse } from "@plane/types"; + +type Props = { + activity: IUserActivityResponse | undefined; +}; + +export const ActivityList: React.FC = observer((props) => { + const { activity } = props; + // store hooks + const { currentUser } = useUser(); + + // TODO: refactor this component + return ( + <> + {activity ? ( +
      + {activity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} + + + +
      +
      +
      +
      + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
      +

      + Commented {calculateTimeAgo(activityItem.created_at)} +

      +
      +
      + +
      +
      +
      +
      + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
    • +
      +
      + <> +
      +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} +
      +
      +
      +
      +
      +
      + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
      + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
      +
      +
      + +
      +
      +
    • + ); + })} +
    + ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/download-button.tsx b/web/components/profile/activity/download-button.tsx new file mode 100644 index 00000000000..ff928dc2ad9 --- /dev/null +++ b/web/components/profile/activity/download-button.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +// services +import { UserService } from "services/user.service"; +// ui +import { Button } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; + +const userService = new UserService(); + +export const DownloadActivityButton = () => { + // states + const [isDownloading, setIsDownloading] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const handleDownload = async () => { + const today = renderFormattedPayloadDate(new Date()); + + if (!workspaceSlug || !userId || !today) return; + + setIsDownloading(true); + + const csv = await userService + .downloadProfileActivity(workspaceSlug.toString(), userId.toString(), { + date: today, + }) + .finally(() => setIsDownloading(false)); + + // create a Blob object + const blob = new Blob([csv], { type: "text/csv" }); + + // create URL for the Blob object + const url = window.URL.createObjectURL(blob); + + // create a link element + const a = document.createElement("a"); + a.href = url; + a.download = `profile-activity-${Date.now()}.csv`; + document.body.appendChild(a); + + // simulate click on the link element to trigger download + a.click(); + + // cleanup + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }; + + return ( + + ); +}; diff --git a/web/components/profile/activity/index.ts b/web/components/profile/activity/index.ts new file mode 100644 index 00000000000..3b202d6c52f --- /dev/null +++ b/web/components/profile/activity/index.ts @@ -0,0 +1,4 @@ +export * from "./activity-list"; +export * from "./download-button"; +export * from "./profile-activity-list"; +export * from "./workspace-activity-list"; diff --git a/web/components/profile/activity/profile-activity-list.tsx b/web/components/profile/activity/profile-activity-list.tsx new file mode 100644 index 00000000000..3912c85680b --- /dev/null +++ b/web/components/profile/activity/profile-activity-list.tsx @@ -0,0 +1,190 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +import { History, MessageSquare } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; +// services +import { UserService } from "services/user.service"; +// editor +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +// components +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +// ui +import { ActivitySettingsLoader } from "components/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +// fetch-keys +import { USER_ACTIVITY } from "constants/fetch-keys"; + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const ProfileActivityListPage: React.FC = observer((props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // store hooks + const { currentUser } = useUser(); + + const { data: userProfileActivity } = useSWR( + USER_ACTIVITY({ + cursor, + }), + () => + userService.getUserActivity({ + cursor, + per_page: perPage, + }) + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + // TODO: refactor this component + return ( + <> + {userProfileActivity ? ( +
      + {userProfileActivity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} + + + +
      +
      +
      +
      + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
      +

      + Commented {calculateTimeAgo(activityItem.created_at)} +

      +
      +
      + +
      +
      +
      +
      + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
    • +
      +
      + <> +
      +
      +
      +
      + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
      + {activityItem.actor_detail.display_name?.[0]} +
      + )} +
      +
      +
      +
      +
      +
      + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
      + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
      +
      +
      + +
      +
      +
    • + ); + })} +
    + ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/workspace-activity-list.tsx b/web/components/profile/activity/workspace-activity-list.tsx new file mode 100644 index 00000000000..c2c75a19564 --- /dev/null +++ b/web/components/profile/activity/workspace-activity-list.tsx @@ -0,0 +1,50 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// services +import { UserService } from "services/user.service"; +// components +import { ActivityList } from "./activity-list"; +// fetch-keys +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const WorkspaceActivityListPage: React.FC = (props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const { data: userProfileActivity } = useSWR( + workspaceSlug && userId + ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), { + cursor, + }) + : null, + workspaceSlug && userId + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + cursor, + per_page: perPage, + }) + : null + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + return ; +}; diff --git a/web/components/profile/index.ts b/web/components/profile/index.ts index f6d2a3775c6..35ac288adb7 100644 --- a/web/components/profile/index.ts +++ b/web/components/profile/index.ts @@ -1,3 +1,4 @@ +export * from "./activity"; export * from "./overview"; export * from "./navbar"; export * from "./profile-issues-filter"; diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 4361b7a9d2f..582f0f26bfb 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -27,10 +27,11 @@ export const ProfileNavbar: React.FC = (props) => { {tabsList.map((tab) => ( {tab.label} diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 58bbb689831..112c073abd2 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -27,15 +27,18 @@ export const ProfileActivity = observer(() => { const { currentUser } = useUser(); const { data: userProfileActivity } = useSWR( - workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString()) : null, + workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {}) : null, workspaceSlug && userId - ? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString()) + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + per_page: 10, + }) : null ); return (
    -

    Recent Activity

    +

    Recent activity

    {userProfileActivity ? ( userProfileActivity.results.length > 0 ? ( diff --git a/web/components/profile/overview/priority-distribution.tsx b/web/components/profile/overview/priority-distribution.tsx deleted file mode 100644 index 8a931183f8c..00000000000 --- a/web/components/profile/overview/priority-distribution.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// ui -import { BarGraph, ProfileEmptyState } from "components/ui"; -import { Loader } from "@plane/ui"; -// image -import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; -// helpers -import { capitalizeFirstLetter } from "helpers/string.helper"; -// types -import { IUserProfileData } from "@plane/types"; - -type Props = { - userProfile: IUserProfileData | undefined; -}; - -export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => ( -
    -

    Issues by Priority

    - {userProfile ? ( -
    - {userProfile.priority_distribution.length > 0 ? ( - ({ - priority: capitalizeFirstLetter(priority.priority ?? "None"), - value: priority.priority_count, - }))} - height="300px" - indexBy="priority" - keys={["value"]} - borderRadius={4} - padding={0.7} - customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} - tooltip={(datum) => ( -
    - - {datum.data.priority}: - {datum.value} -
    - )} - colors={(datum) => { - if (datum.data.priority === "Urgent") return "#991b1b"; - else if (datum.data.priority === "High") return "#ef4444"; - else if (datum.data.priority === "Medium") return "#f59e0b"; - else if (datum.data.priority === "Low") return "#16a34a"; - else return "#e5e5e5"; - }} - theme={{ - axis: { - domain: { - line: { - stroke: "transparent", - }, - }, - }, - grid: { - line: { - stroke: "transparent", - }, - }, - }} - /> - ) : ( -
    - -
    - )} -
    - ) : ( -
    - - - - - - - -
    - )} -
    -); diff --git a/web/components/profile/overview/priority-distribution/index.ts b/web/components/profile/overview/priority-distribution/index.ts new file mode 100644 index 00000000000..64d81eb1244 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/index.ts @@ -0,0 +1 @@ +export * from "./priority-distribution"; diff --git a/web/components/profile/overview/priority-distribution/main-content.tsx b/web/components/profile/overview/priority-distribution/main-content.tsx new file mode 100644 index 00000000000..8606f44b1e6 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/main-content.tsx @@ -0,0 +1,31 @@ +// components +import { IssuesByPriorityGraph } from "components/graphs"; +import { ProfileEmptyState } from "components/ui"; +// assets +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; +// types +import { IUserPriorityDistribution } from "@plane/types"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[]; +}; + +export const PriorityDistributionContent: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +
    + {priorityDistribution.length > 0 ? ( + + ) : ( +
    + +
    + )} +
    + ); +}; diff --git a/web/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/components/profile/overview/priority-distribution/priority-distribution.tsx new file mode 100644 index 00000000000..63559bdeee6 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/priority-distribution.tsx @@ -0,0 +1,33 @@ +// components +import { PriorityDistributionContent } from "./main-content"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IUserPriorityDistribution } from "@plane/types"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[] | undefined; +}; + +export const ProfilePriorityDistribution: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +
    +

    Issues by priority

    + {priorityDistribution ? ( + + ) : ( +
    + + + + + + + +
    + )} +
    + ); +}; diff --git a/web/components/profile/overview/state-distribution.tsx b/web/components/profile/overview/state-distribution.tsx index 5664637e9d3..f38283aa738 100644 --- a/web/components/profile/overview/state-distribution.tsx +++ b/web/components/profile/overview/state-distribution.tsx @@ -17,7 +17,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u return (
    -

    Issues by State

    +

    Issues by state

    {userProfile.state_distribution.length > 0 ? (
    @@ -65,7 +65,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u backgroundColor: STATE_GROUPS[group.state_group].color, }} /> -
    {group.state_group}
    +
    {STATE_GROUPS[group.state_group].label}
    {group.state_count}
    diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index c091a94f77a..86989748d7c 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -21,12 +21,12 @@ export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( }} />
    -

    +

    {group.state_group === "unstarted" - ? "Not Started" + ? "Not started" : group.state_group === "started" ? "Working on" - : group.state_group} + : STATE_GROUPS[group.state_group].label}

    {group.state_count}

    diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 71d935d3c80..48bb7d32382 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -1,9 +1,11 @@ +import { useEffect, useRef } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import useSWR from "swr"; import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { useApplication, useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; @@ -18,8 +20,6 @@ import { renderFormattedDate } from "helpers/date-time.helper"; import { renderEmoji } from "helpers/emoji.helper"; // fetch-keys import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useEffect, useRef } from "react"; // services const userService = new UserService(); diff --git a/web/components/ui/graphs/index.ts b/web/components/ui/graphs/index.ts index 1f40adbffa5..984bb642cf2 100644 --- a/web/components/ui/graphs/index.ts +++ b/web/components/ui/graphs/index.ts @@ -1,6 +1,5 @@ export * from "./bar-graph"; export * from "./calendar-graph"; export * from "./line-graph"; -export * from "./marimekko-graph"; export * from "./pie-graph"; export * from "./scatter-plot-graph"; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx deleted file mode 100644 index fd460d11b3b..00000000000 --- a/web/components/ui/graphs/marimekko-graph.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// nivo -import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; -// helpers -import { generateYAxisTickValues } from "helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; - -type Props = { - id: string; - value: string; - customYAxisTickValues?: number[]; -}; - -export const MarimekkoGraph: React.FC, "height" | "width">> = ({ - id, - value, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
    - 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - {...rest} - /> -
    -); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index b1cfa51d73c..6ac4e78174c 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -7,7 +7,7 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; // types -import { TDurationFilterOptions, TIssuesListTypes, TStateGroups } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; @@ -118,29 +118,33 @@ export const STATE_GROUP_GRAPH_COLORS: Record = { // filter duration options export const DURATION_FILTER_OPTIONS: { - key: TDurationFilterOptions; + key: EDurationFilters; label: string; }[] = [ { - key: "none", + key: EDurationFilters.NONE, label: "None", }, { - key: "today", + key: EDurationFilters.TODAY, label: "Due today", }, { - key: "this_week", - label: " Due this week", + key: EDurationFilters.THIS_WEEK, + label: "Due this week", }, { - key: "this_month", + key: EDurationFilters.THIS_MONTH, label: "Due this month", }, { - key: "this_year", + key: EDurationFilters.THIS_YEAR, label: "Due this year", }, + { + key: EDurationFilters.CUSTOM, + label: "Custom", + }, ]; // random background colors for project cards diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 86386e9683c..3b2e97c38cb 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -164,7 +164,7 @@ export const USER_ISSUES = (workspaceSlug: string, params: any) => { return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`; }; -export const USER_ACTIVITY = "USER_ACTIVITY"; +export const USER_ACTIVITY = (params: { cursor?: string }) => `USER_ACTIVITY_${params?.cursor}`; export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`; @@ -284,8 +284,13 @@ export const getPaginatedNotificationKey = (index: number, prevData: any, worksp // profile export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) => `USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; -export const USER_PROFILE_ACTIVITY = (workspaceSlug: string, userId: string) => - `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; +export const USER_PROFILE_ACTIVITY = ( + workspaceSlug: string, + userId: string, + params: { + cursor?: string; + } +) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`; export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) => `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => { diff --git a/web/constants/profile.ts b/web/constants/profile.ts index 463fd27eed9..4d8e37640a2 100644 --- a/web/constants/profile.ts +++ b/web/constants/profile.ts @@ -63,4 +63,9 @@ export const PROFILE_ADMINS_TAB = [ label: "Subscribed", selected: "/[workspaceSlug]/profile/[userId]/subscribed", }, + { + route: "activity", + label: "Activity", + selected: "/[workspaceSlug]/profile/[userId]/activity", + }, ]; diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 90319a90b90..a61ec7f782a 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -1,36 +1,40 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from "date-fns"; // helpers -import { renderFormattedPayloadDate } from "./date-time.helper"; +import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +// constants +import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; /** * @description returns date range based on the duration filter * @param duration */ -export const getCustomDates = (duration: TDurationFilterOptions): string => { +export const getCustomDates = (duration: EDurationFilters, customDates: string[]): string => { const today = new Date(); let firstDay, lastDay; switch (duration) { - case "none": + case EDurationFilters.NONE: return ""; - case "today": + case EDurationFilters.TODAY: firstDay = renderFormattedPayloadDate(today); lastDay = renderFormattedPayloadDate(today); return `${firstDay};after,${lastDay};before`; - case "this_week": + case EDurationFilters.THIS_WEEK: firstDay = renderFormattedPayloadDate(startOfWeek(today)); lastDay = renderFormattedPayloadDate(endOfWeek(today)); return `${firstDay};after,${lastDay};before`; - case "this_month": + case EDurationFilters.THIS_MONTH: firstDay = renderFormattedPayloadDate(startOfMonth(today)); lastDay = renderFormattedPayloadDate(endOfMonth(today)); return `${firstDay};after,${lastDay};before`; - case "this_year": + case EDurationFilters.THIS_YEAR: firstDay = renderFormattedPayloadDate(startOfYear(today)); lastDay = renderFormattedPayloadDate(endOfYear(today)); return `${firstDay};after,${lastDay};before`; + case EDurationFilters.CUSTOM: + return customDates.join(","); } }; @@ -58,7 +62,7 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { * @param duration * @param tab */ -export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => { +export const getTabKey = (duration: EDurationFilters, tab: TIssuesListTypes | undefined): TIssuesListTypes => { if (!tab) return "completed"; if (tab === "completed") return tab; @@ -69,3 +73,21 @@ export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListType else return "upcoming"; } }; + +/** + * @description returns the label for the duration filter dropdown + * @param duration + * @param customDates + */ +export const getDurationFilterDropdownLabel = (duration: EDurationFilters, customDates: string[]): string => { + if (duration !== "custom") return DURATION_FILTER_OPTIONS.find((option) => option.key === duration)?.label ?? ""; + else { + const afterDate = customDates.find((date) => date.includes("after"))?.split(";")[0]; + const beforeDate = customDates.find((date) => date.includes("before"))?.split(";")[0]; + + if (afterDate && beforeDate) return `${renderFormattedDate(afterDate)} - ${renderFormattedDate(beforeDate)}`; + else if (afterDate) return `After ${renderFormattedDate(afterDate)}`; + else if (beforeDate) return `Before ${renderFormattedDate(beforeDate)}`; + else return ""; + } +}; diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 60c17d8d4ef..52bfc6fbf8e 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -4,6 +4,8 @@ import { observer } from "mobx-react-lite"; import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { children: React.ReactNode; @@ -11,27 +13,25 @@ type Props = { showProfileIssuesFilter?: boolean; }; -const AUTHORIZED_ROLES = [20, 15, 10]; +const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER]; export const ProfileAuthWrapper: React.FC = observer((props) => { const { children, className, showProfileIssuesFilter } = props; + // router const router = useRouter(); - + // store hooks const { membership: { currentWorkspaceRole }, } = useUser(); - - if (!currentWorkspaceRole) return null; - - const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole); - + // derived values + const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole); const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); return (
    - + {isAuthorized || !isAuthorizedPath ? (
    {children}
    ) : ( diff --git a/web/package.json b/web/package.json index af28cbb3d49..fbec571ef8f 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,6 @@ "@nivo/core": "0.80.0", "@nivo/legends": "0.80.0", "@nivo/line": "0.80.0", - "@nivo/marimekko": "0.80.0", "@nivo/pie": "0.80.0", "@nivo/scatterplot": "0.80.0", "@plane/document-editor": "*", diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx new file mode 100644 index 00000000000..09269676a8d --- /dev/null +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -0,0 +1,84 @@ +import { ReactElement, useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useUser } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +// components +import { UserProfileHeader } from "components/headers"; +import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; +// ui +import { Button } from "@plane/ui"; +// types +import { NextPageWithLayout } from "lib/types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; + +const PER_PAGE = 100; + +const ProfileActivityPage: NextPageWithLayout = observer(() => { + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + // router + const router = useRouter(); + const { userId } = router.query; + // store hooks + const { + currentUser, + membership: { currentWorkspaceRole }, + } = useUser(); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + + const canDownloadActivity = + currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + + return ( +
    +
    +

    Recent activity

    + {canDownloadActivity && } +
    +
    + {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} +
    +
    + ); +}); + +ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { + return ( + }> + {page} + + ); +}; + +export default ProfileActivityPage; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index 7d24a8b1117..6e8a10b5073 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -49,7 +49,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
    - +
    diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 3ea65ed249b..b0e8bb1a028 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,191 +1,64 @@ -import { ReactElement } from "react"; -import useSWR from "swr"; -import Link from "next/link"; +import { ReactElement, useState } from "react"; import { observer } from "mobx-react"; //hooks -import { useApplication, useUser } from "hooks/store"; -// services -import { UserService } from "services/user.service"; +import { useApplication } from "hooks/store"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core"; -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; -// icons -import { History, MessageSquare } from "lucide-react"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ProfileActivityListPage } from "components/profile"; +import { PageHead } from "components/core"; // ui -import { ActivitySettingsLoader } from "components/ui"; -// fetch-keys -import { USER_ACTIVITY } from "constants/fetch-keys"; -// helper -import { calculateTimeAgo } from "helpers/date-time.helper"; +import { Button } from "@plane/ui"; // type import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -const userService = new UserService(); +const PER_PAGE = 100; const ProfileActivityPage: NextPageWithLayout = observer(() => { - const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); // store hooks - const { currentUser } = useUser(); const { theme: themeStore } = useApplication(); + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + return ( <> -
    +
    themeStore.toggleSidebar()} />

    Activity

    - {userActivity ? ( -
    -
      - {userActivity.results.map((activityItem: any) => { - if (activityItem.field === "comment") { - return ( -
      -
      -
      - {activityItem.field ? ( - activityItem.new_value === "restore" && ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
      - {activityItem.actor_detail.display_name?.charAt(0)} -
      - )} - - - -
      -
      -
      -
      - {activityItem.actor_detail.is_bot - ? activityItem.actor_detail.first_name + " Bot" - : activityItem.actor_detail.display_name} -
      -

      - Commented {calculateTimeAgo(activityItem.created_at)} -

      -
      -
      - -
      -
      -
      -
      - ); - } - - const message = - activityItem.verb === "created" && - activityItem.field !== "cycles" && - activityItem.field !== "modules" && - activityItem.field !== "attachment" && - activityItem.field !== "link" && - activityItem.field !== "estimate" && - !activityItem.field ? ( - - created - - ) : ( - - ); - - if ("field" in activityItem && activityItem.field !== "updated_by") { - return ( -
    • -
      -
      - <> -
      -
      -
      -
      - {activityItem.field ? ( - activityItem.new_value === "restore" ? ( - - ) : ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
      - {activityItem.actor_detail.display_name?.charAt(0)} -
      - )} -
      -
      -
      -
      -
      -
      - {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( - Plane - ) : activityItem.actor_detail.is_bot ? ( - - {activityItem.actor_detail.first_name} Bot - - ) : ( - - - {currentUser?.id === activityItem.actor_detail.id - ? "You" - : activityItem.actor_detail.display_name} - - - )}{" "} -
      - {message}{" "} - - {calculateTimeAgo(activityItem.created_at)} - -
      -
      -
      - -
      -
      -
    • - ); - } - })} -
    -
    - ) : ( - - )} +
    + {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
    + +
    + )} +
    ); diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 13ffa9c51ec..41111db98c9 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -9,7 +9,6 @@ import type { IUserProfileData, IUserProfileProjectSegregation, IUserSettings, - IUserWorkspaceDashboard, IUserEmailNotificationSettings, } from "@plane/types"; // helpers @@ -113,20 +112,8 @@ export class UserService extends APIService { }); } - async getUserActivity(): Promise { - return this.get(`/api/users/me/activities/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async userWorkspaceDashboard(workspaceSlug: string, month: number): Promise { - return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, { - params: { - month: month, - }, - }) + async getUserActivity(params: { per_page: number; cursor?: string }): Promise { + return this.get("/api/users/me/activities/", { params }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -160,8 +147,31 @@ export class UserService extends APIService { }); } - async getUserProfileActivity(workspaceSlug: string, userId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`) + async getUserProfileActivity( + workspaceSlug: string, + userId: string, + params: { + per_page: number; + cursor?: string; + } + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async downloadProfileActivity( + workspaceSlug: string, + userId: string, + data: { + date: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/export/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/user/index.ts b/web/store/user/index.ts index 15f9e57728e..ada2e6be7b9 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -22,7 +22,6 @@ export interface IUserRootStore { fetchCurrentUser: () => Promise; fetchCurrentUserInstanceAdminStatus: () => Promise; fetchCurrentUserSettings: () => Promise; - fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise; // crud actions updateUserOnBoard: () => Promise; updateTourCompleted: () => Promise; @@ -68,7 +67,6 @@ export class UserRootStore implements IUserRootStore { fetchCurrentUser: action, fetchCurrentUserInstanceAdminStatus: action, fetchCurrentUserSettings: action, - fetchUserDashboardInfo: action, updateUserOnBoard: action, updateTourCompleted: action, updateCurrentUser: action, @@ -130,22 +128,6 @@ export class UserRootStore implements IUserRootStore { return response; }); - /** - * Fetches the current user dashboard info - * @returns Promise - */ - fetchUserDashboardInfo = async (workspaceSlug: string, month: number) => { - try { - const response = await this.userService.userWorkspaceDashboard(workspaceSlug, month); - runInAction(() => { - this.dashboardInfo = response; - }); - return response; - } catch (error) { - throw error; - } - }; - /** * Updates the user onboarding status * @returns Promise diff --git a/yarn.lock b/yarn.lock index 81e6224e84d..0a21fcee2ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,19 +1766,6 @@ "@react-spring/web" "9.4.5" d3-shape "^1.3.5" -"@nivo/marimekko@0.80.0": - version "0.80.0" - resolved "https://registry.yarnpkg.com/@nivo/marimekko/-/marimekko-0.80.0.tgz#1eda4207935b3776bd1d7d729dc39a0e42bb1b86" - integrity sha512-0u20SryNtbOQhtvhZsbxmgnF7o8Yc2rjDQ/gCYPTXtxeooWCuhSaRZbDCnCeyKQY3B62D7z2mu4Js4KlTEftjA== - dependencies: - "@nivo/axes" "0.80.0" - "@nivo/colors" "0.80.0" - "@nivo/legends" "0.80.0" - "@nivo/scales" "0.80.0" - "@react-spring/web" "9.4.5" - d3-shape "^1.3.5" - lodash "^4.17.21" - "@nivo/pie@0.80.0": version "0.80.0" resolved "https://registry.yarnpkg.com/@nivo/pie/-/pie-0.80.0.tgz#04b35839bf5a2b661fa4e5b677ae76b3c028471e" From 4572b7378df0e4e5ad1246cb5f8cdb39bbbfde51 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:24:58 +0530 Subject: [PATCH 011/214] [WEB-621] chore: 404 page not found (#3859) * chore: custom 404 error * chore: moved from middleware to view --- apiserver/plane/app/views/__init__.py | 4 +++- apiserver/plane/app/views/error_404.py | 5 +++++ apiserver/plane/urls.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 apiserver/plane/app/views/error_404.py diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 6af60ff9c18..910ea006d6d 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -187,4 +187,6 @@ from .dashboard import ( DashboardEndpoint, WidgetsEndpoint -) \ No newline at end of file +) + +from .error_404 import custom_404_view diff --git a/apiserver/plane/app/views/error_404.py b/apiserver/plane/app/views/error_404.py new file mode 100644 index 00000000000..3c31474e0a5 --- /dev/null +++ b/apiserver/plane/app/views/error_404.py @@ -0,0 +1,5 @@ +# views.py +from django.http import JsonResponse + +def custom_404_view(request, exception=None): + return JsonResponse({"error": "Page not found."}, status=404) diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 669f3ea73de..3b042ea1fa1 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -7,6 +7,7 @@ from django.conf import settings +handler404 = "plane.app.views.error_404.custom_404_view" urlpatterns = [ path("", TemplateView.as_view(template_name="index.html")), From dbdd14493b05ae502380762d50a53369b124fd34 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:25:15 +0530 Subject: [PATCH 012/214] [WEB-662] chore: posthog debugging based on env variable (#3860) * chore: posthog debugging based on env variable * updated turbo.json * chore: true to 1 --------- Co-authored-by: gurusainath --- turbo.json | 1 + web/lib/posthog-provider.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/turbo.json b/turbo.json index bd5ee34b59e..9302a71831b 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,7 @@ "NEXT_PUBLIC_DEPLOY_WITH_NGINX", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST", + "NEXT_PUBLIC_POSTHOG_DEBUG", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST" ], diff --git a/web/lib/posthog-provider.tsx b/web/lib/posthog-provider.tsx index e8c1b7899a4..c5acd295761 100644 --- a/web/lib/posthog-provider.tsx +++ b/web/lib/posthog-provider.tsx @@ -45,6 +45,7 @@ const PostHogProvider: FC = (props) => { if (posthogAPIKey && posthogHost) { posthog.init(posthogAPIKey, { api_host: posthogHost || "https://app.posthog.com", + debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable autocapture: false, capture_pageview: false, // Disable automatic pageview capture, as we capture manually }); From 7b76df68680f08eb416c93f24ba9ac2d7b7a1eee Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:26:34 +0530 Subject: [PATCH 013/214] [WEB-581] chore: project states setting page improvement (#3864) * chore: project states setting page improvement * chore: code refactor * chore: observer added in project state setting page --- .../projects/[projectId]/settings/states.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 3fa9561a8a6..57451e69992 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,21 +1,34 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layout import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components import { ProjectSettingStateList } from "components/states"; import { ProjectSettingHeader } from "components/headers"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; +// hook +import { useProject } from "hooks/store"; -const StatesSettingsPage: NextPageWithLayout = () => ( -
    -
    -

    States

    -
    - -
    -); +const StatesSettingsPage: NextPageWithLayout = observer(() => { + // store + const { currentProjectDetails } = useProject(); + // derived values + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + return ( + <> + +
    +
    +

    States

    +
    + +
    + + ); +}); StatesSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( From 666b7ea5775a0e24b72584725bbb78aac09eeb5b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:27:41 +0530 Subject: [PATCH 014/214] fix: remove member button alignment (#3862) --- .../send-workspace-invitation-modal.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 35b5963d097..25f4c3c7298 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -121,7 +121,7 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) =>
    {fields.map((field, index) => ( -
    +
    = observer((props) => )} />
    -
    +
    = observer((props) => label={{ROLE[value]}} onChange={onChange} optionsClassName="w-full" + className="flex-grow" input > {Object.entries(ROLE).map(([key, value]) => { @@ -179,16 +180,16 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) => )} /> + {fields.length > 1 && ( + + )}
    - {fields.length > 1 && ( - - )}
    ))}
    From 69fa1708cc79363de38231c4d9f2de544a0aa8b8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:27:59 +0530 Subject: [PATCH 015/214] fix: module quick action dropdown overflow fix (#3865) --- web/components/modules/module-card-item.tsx | 2 +- web/components/modules/module-list-item.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 52cc6097be3..dbbde56d785 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -265,7 +265,7 @@ export const ModuleCardItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 72ed16adf1e..63e780cb2e4 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -255,7 +255,7 @@ export const ModuleListItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> From 2b05d23470770de4e15463cd1961c6491550015b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:28:24 +0530 Subject: [PATCH 016/214] fix: kanban card state overflow fix (#3866) --- .../issues/issue-layouts/properties/all-properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 776a1cd46f2..238d2e74457 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -240,7 +240,7 @@ export const IssueProperties: React.FC = observer((props) => { {/* basic properties */} {/* state */} -
    +
    Date: Wed, 6 Mar 2024 14:29:52 +0530 Subject: [PATCH 017/214] [WEB-566] feat: Added text to empty color picker boxes and other fixes (#3867) * fix: z index issues with modals and on hover color in table item picker menu * feat: added text indicators inside the table colors to give a gist of how text would look --- packages/editor/core/src/styles/table.css | 6 +++--- .../core/src/ui/extensions/table/table/table-view.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/editor/core/src/styles/table.css b/packages/editor/core/src/styles/table.css index ca384d34fc6..3ba17ee1b28 100644 --- a/packages/editor/core/src/styles/table.css +++ b/packages/editor/core/src/styles/table.css @@ -98,7 +98,7 @@ top: 0; bottom: -2px; width: 4px; - z-index: 99; + z-index: 5; background-color: #d9e4ff; pointer-events: none; } @@ -111,7 +111,7 @@ .tableWrapper .tableControls .rowsControl { transition: opacity ease-in 100ms; position: absolute; - z-index: 99; + z-index: 5; display: flex; justify-content: center; align-items: center; @@ -198,7 +198,7 @@ .tableWrapper .tableControls .tableToolbox .toolboxItem:hover, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover { - background-color: rgba(var(--color-background-100), 0.5); + background-color: rgba(var(--color-background-80), 0.6); } .tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer, diff --git a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx index 674a8e1150a..2941179c7c5 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx +++ b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx @@ -213,10 +213,11 @@ function createToolbox({ { className: "colorPicker grid" }, Object.entries(colors).map(([colorName, colorValue]) => h("div", { - className: "colorPickerItem", + className: "colorPickerItem flex items-center justify-center", style: `background-color: ${colorValue.backgroundColor}; - color: ${colorValue.textColor || "inherit"};`, - innerHTML: colorValue?.icon || "", + color: ${colorValue.textColor || "inherit"};`, + innerHTML: + colorValue.icon ?? `A`, onClick: () => onSelectColor(colorValue), }) ) From 3d09a69d5803369fe2945de51dacf0e52d46bce6 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 18:39:14 +0530 Subject: [PATCH 018/214] fix: eslint issues and reconfiguring (#3891) * fix: eslint fixes --------- Co-authored-by: gurusainath --- CONTRIBUTING.md | 1 - ENV_SETUP.md | 1 - README.md | 36 +- deploy/1-click/README.md | 30 +- packages/editor/core/package.json | 3 +- packages/editor/document-editor/package.json | 3 +- .../src/ui/components/content-browser.tsx | 2 +- .../src/ui/components/summary-popover.tsx | 5 +- .../src/ui/extensions/index.tsx | 1 - packages/editor/extensions/package.json | 3 +- packages/editor/lite-text-editor/package.json | 3 +- packages/editor/rich-text-editor/package.json | 2 +- packages/eslint-config-custom/index.js | 33 +- packages/eslint-config-custom/package.json | 22 +- packages/types/src/pages.d.ts | 7 +- packages/types/src/state.d.ts | 7 +- packages/types/src/views.d.ts | 6 +- packages/ui/src/badge/helper.tsx | 10 +- packages/ui/src/breadcrumbs/breadcrumbs.tsx | 15 +- packages/ui/src/button/helper.tsx | 10 +- packages/ui/src/control-link/control-link.tsx | 4 +- space/components/ui/dropdown.tsx | 6 +- space/lib/mobx/store-provider.tsx | 8 +- space/package.json | 2 - web/.eslintrc.js | 99 +++ .../account/deactivate-account-modal.tsx | 8 +- .../account/o-auth/o-auth-options.tsx | 6 +- .../sign-in-forms/optional-set-password.tsx | 2 +- .../account/sign-in-forms/password.tsx | 12 +- web/components/account/sign-in-forms/root.tsx | 12 +- .../account/sign-in-forms/unique-code.tsx | 12 +- .../account/sign-up-forms/email.tsx | 6 +- .../sign-up-forms/optional-set-password.tsx | 14 +- .../account/sign-up-forms/password.tsx | 6 +- web/components/account/sign-up-forms/root.tsx | 10 +- .../account/sign-up-forms/unique-code.tsx | 11 +- .../custom-analytics/custom-analytics.tsx | 8 +- .../custom-analytics/graph/custom-tooltip.tsx | 4 +- .../custom-analytics/graph/index.tsx | 8 +- .../custom-analytics/main-content.tsx | 4 +- .../analytics/custom-analytics/select-bar.tsx | 7 +- .../custom-analytics/select/project.tsx | 2 +- .../custom-analytics/select/segment.tsx | 2 +- .../custom-analytics/select/x-axis.tsx | 2 +- .../custom-analytics/select/y-axis.tsx | 2 +- .../sidebar/projects-list.tsx | 2 +- .../sidebar/sidebar-header.tsx | 8 +- .../custom-analytics/sidebar/sidebar.tsx | 20 +- .../analytics/custom-analytics/table.tsx | 2 +- .../analytics/project-modal/main-content.tsx | 9 +- .../analytics/project-modal/modal.tsx | 10 +- .../analytics/scope-and-demand/demand.tsx | 2 +- .../scope-and-demand/scope-and-demand.tsx | 4 +- .../scope-and-demand/year-wise-issues.tsx | 2 +- .../api-token/delete-token-modal.tsx | 4 +- .../api-token/modal/create-token-modal.tsx | 11 +- web/components/api-token/token-list-item.tsx | 2 +- .../auth-screens/not-authorized-view.tsx | 4 +- .../auth-screens/project/join-project.tsx | 4 +- .../auth-screens/workspace/not-a-member.tsx | 2 +- .../automation/auto-archive-automation.tsx | 4 +- .../automation/auto-close-automation.tsx | 8 +- web/components/breadcrumbs/index.tsx | 2 +- .../command-palette/actions/help-actions.tsx | 2 +- .../actions/issue-actions/actions-list.tsx | 10 +- .../actions/issue-actions/change-assignee.tsx | 8 +- .../actions/issue-actions/change-priority.tsx | 8 +- .../actions/issue-actions/change-state.tsx | 10 +- .../actions/project-actions.tsx | 2 +- .../actions/search-results.tsx | 2 +- .../command-palette/actions/theme-actions.tsx | 6 +- .../actions/workspace-settings-actions.tsx | 4 +- .../command-palette/command-modal.tsx | 22 +- .../command-palette/command-palette.tsx | 15 +- web/components/command-palette/helpers.tsx | 2 +- .../command-palette/shortcuts-modal/modal.tsx | 2 +- web/components/common/breadcrumb-link.tsx | 2 +- .../common/product-updates-modal.tsx | 6 +- web/components/core/activity.tsx | 6 +- .../core/filters/date-filter-modal.tsx | 15 +- web/components/core/image-picker-popover.tsx | 12 +- .../core/modals/bulk-delete-issues-modal.tsx | 15 +- .../modals/existing-issues-list-modal.tsx | 11 +- .../core/modals/gpt-assistant-popover.tsx | 14 +- web/components/core/modals/link-modal.tsx | 4 +- .../core/modals/user-image-upload-modal.tsx | 7 +- .../modals/workspace-image-upload-modal.tsx | 8 +- web/components/core/render-if-visible-HOC.tsx | 11 +- web/components/core/sidebar/links-list.tsx | 11 +- .../sidebar/sidebar-menu-hamburger-toggle.tsx | 2 +- .../core/sidebar/sidebar-progress-stats.tsx | 4 +- .../core/theme/color-picker-input.tsx | 4 +- .../core/theme/custom-theme-selector.tsx | 4 +- web/components/core/theme/theme-switch.tsx | 2 +- .../cycles/active-cycle-details.tsx | 20 +- web/components/cycles/active-cycle-stats.tsx | 4 +- web/components/cycles/cycle-mobile-header.tsx | 9 +- web/components/cycles/cycle-peek-overview.tsx | 2 +- web/components/cycles/cycles-board-card.tsx | 18 +- web/components/cycles/cycles-board.tsx | 2 +- web/components/cycles/cycles-list-item.tsx | 24 +- web/components/cycles/cycles-list.tsx | 4 +- web/components/cycles/cycles-view.tsx | 10 +- web/components/cycles/delete-modal.tsx | 6 +- web/components/cycles/form.tsx | 2 +- web/components/cycles/gantt-chart/blocks.tsx | 28 +- .../cycles/gantt-chart/cycles-list-layout.tsx | 8 +- web/components/cycles/modal.tsx | 10 +- web/components/cycles/sidebar.tsx | 35 +- .../cycles/transfer-issues-modal.tsx | 9 +- web/components/cycles/transfer-issues.tsx | 6 +- .../dashboard/home-dashboard-widgets.tsx | 4 +- .../dashboard/project-empty-state.tsx | 6 +- .../dashboard/widgets/assigned-issues.tsx | 10 +- .../dashboard/widgets/created-issues.tsx | 10 +- .../widgets/dropdowns/duration-filter.tsx | 3 +- .../widgets/empty-states/assigned-issues.tsx | 2 +- .../widgets/empty-states/created-issues.tsx | 2 +- .../widgets/issue-panels/issue-list-item.tsx | 4 +- .../widgets/issue-panels/issues-list.tsx | 4 +- .../widgets/issue-panels/tabs-list.tsx | 2 +- .../dashboard/widgets/issues-by-priority.tsx | 16 +- .../widgets/issues-by-state-group.tsx | 30 +- .../dashboard/widgets/loaders/loader.tsx | 4 +- .../dashboard/widgets/overview-stats.tsx | 7 +- .../dashboard/widgets/recent-activity.tsx | 20 +- .../widgets/recent-collaborators.tsx | 94 +++ .../collaborators-list.tsx | 6 +- .../recent-collaborators/default-list.tsx | 2 +- .../widgets/recent-collaborators/root.tsx | 11 +- .../recent-collaborators/search-list.tsx | 2 +- .../dashboard/widgets/recent-projects.tsx | 12 +- web/components/dropdowns/buttons.tsx | 8 +- .../dropdowns/cycle/cycle-options.tsx | 26 +- web/components/dropdowns/cycle/index.tsx | 6 +- web/components/dropdowns/date-range.tsx | 10 +- web/components/dropdowns/date.tsx | 8 +- web/components/dropdowns/estimate.tsx | 8 +- web/components/dropdowns/member/avatar.tsx | 2 +- web/components/dropdowns/member/index.tsx | 8 +- .../dropdowns/member/member-options.tsx | 8 +- web/components/dropdowns/module/index.tsx | 17 +- .../dropdowns/module/module-options.tsx | 10 +- web/components/dropdowns/priority.tsx | 16 +- web/components/dropdowns/project.tsx | 16 +- web/components/dropdowns/state.tsx | 8 +- web/components/emoji-icon-picker/index.tsx | 8 +- .../empty-state/comic-box-button.tsx | 2 +- web/components/empty-state/empty-state.tsx | 2 +- .../create-update-estimate-modal.tsx | 8 +- .../estimates/delete-estimate-modal.tsx | 5 +- .../estimates/estimate-list-item.tsx | 8 +- web/components/estimates/estimates-list.tsx | 12 +- web/components/exporter/export-modal.tsx | 5 +- web/components/exporter/guide.tsx | 21 +- web/components/exporter/single-export.tsx | 12 +- web/components/gantt-chart/blocks/block.tsx | 8 +- .../gantt-chart/blocks/blocks-list.tsx | 5 +- web/components/gantt-chart/chart/header.tsx | 6 +- .../gantt-chart/chart/main-content.tsx | 2 +- web/components/gantt-chart/chart/root.tsx | 16 +- .../gantt-chart/chart/views/month.tsx | 2 +- web/components/gantt-chart/contexts/index.tsx | 16 +- .../gantt-chart/helpers/add-block.tsx | 4 +- .../gantt-chart/helpers/draggable.tsx | 2 +- .../gantt-chart/sidebar/cycles/block.tsx | 6 +- .../gantt-chart/sidebar/cycles/sidebar.tsx | 2 +- .../gantt-chart/sidebar/issues/block.tsx | 4 +- .../gantt-chart/sidebar/issues/sidebar.tsx | 2 +- .../gantt-chart/sidebar/modules/block.tsx | 4 +- .../gantt-chart/sidebar/modules/sidebar.tsx | 2 +- .../gantt-chart/sidebar/project-views.tsx | 2 +- .../gantt-chart/views/bi-week-view.ts | 2 +- web/components/gantt-chart/views/day-view.ts | 2 +- web/components/gantt-chart/views/helpers.ts | 4 +- .../gantt-chart/views/hours-view.ts | 2 +- .../gantt-chart/views/month-view.ts | 4 +- .../gantt-chart/views/quater-view.ts | 2 +- web/components/gantt-chart/views/week-view.ts | 2 +- web/components/gantt-chart/views/year-view.ts | 2 +- web/components/graphs/issues-by-priority.tsx | 6 +- web/components/headers/cycle-issues.tsx | 30 +- web/components/headers/cycles.tsx | 17 +- web/components/headers/global-issues.tsx | 16 +- web/components/headers/module-issues.tsx | 30 +- web/components/headers/modules-list.tsx | 40 +- web/components/headers/page-details.tsx | 8 +- web/components/headers/pages.tsx | 10 +- web/components/headers/profile-settings.tsx | 2 +- .../project-archived-issue-details.tsx | 16 +- .../headers/project-archived-issues.tsx | 10 +- .../headers/project-draft-issues.tsx | 12 +- web/components/headers/project-inbox.tsx | 8 +- .../headers/project-issue-details.tsx | 22 +- web/components/headers/project-issues.tsx | 22 +- web/components/headers/project-settings.tsx | 8 +- .../headers/project-view-issues.tsx | 30 +- web/components/headers/project-views.tsx | 8 +- web/components/headers/projects.tsx | 6 +- web/components/headers/user-profile.tsx | 109 +-- .../headers/workspace-active-cycles.tsx | 2 +- .../headers/workspace-analytics.tsx | 26 +- .../headers/workspace-dashboard.tsx | 6 +- web/components/headers/workspace-settings.tsx | 6 +- web/components/icons/priority-icon.tsx | 12 +- .../icons/state/state-group-icon.tsx | 2 +- web/components/inbox/content/root.tsx | 6 +- web/components/inbox/inbox-issue-actions.tsx | 22 +- web/components/inbox/inbox-issue-status.tsx | 2 +- .../inbox/modals/accept-issue-modal.tsx | 2 +- .../inbox/modals/create-issue-modal.tsx | 16 +- .../inbox/modals/decline-issue-modal.tsx | 2 +- .../inbox/modals/delete-issue-modal.tsx | 2 +- .../inbox/modals/select-duplicate.tsx | 8 +- .../inbox/sidebar/filter/applied-filters.tsx | 16 +- .../inbox/sidebar/filter/filter-selection.tsx | 6 +- .../inbox/sidebar/inbox-list-item.tsx | 4 +- web/components/inbox/sidebar/inbox-list.tsx | 8 +- web/components/inbox/sidebar/root.tsx | 4 +- web/components/instance/ai-form.tsx | 1 - web/components/instance/email-form.tsx | 3 +- web/components/instance/general-form.tsx | 1 - .../instance/github-config-form.tsx | 1 - .../instance/google-config-form.tsx | 1 - web/components/instance/help-section.tsx | 4 +- web/components/instance/image-config-form.tsx | 3 +- .../instance/instance-admin-restriction.tsx | 6 +- web/components/instance/not-ready-view.tsx | 2 +- web/components/instance/setup-done-view.tsx | 4 +- .../instance/setup-form/sign-in-form.tsx | 6 +- web/components/instance/sidebar-dropdown.tsx | 6 +- web/components/instance/sidebar-menu.tsx | 2 +- .../integration/delete-import-modal.tsx | 6 +- web/components/integration/github/auth.tsx | 2 +- .../integration/github/import-data.tsx | 4 +- .../integration/github/repo-details.tsx | 5 +- web/components/integration/github/root.tsx | 15 +- .../integration/github/select-repository.tsx | 2 +- .../integration/github/single-user-select.tsx | 4 +- web/components/integration/guide.tsx | 22 +- .../integration/jira/give-details.tsx | 4 +- .../integration/jira/import-users.tsx | 6 +- .../integration/jira/jira-project-detail.tsx | 6 +- web/components/integration/jira/root.tsx | 16 +- web/components/integration/single-import.tsx | 12 +- .../integration/single-integration-card.tsx | 16 +- .../integration/slack/select-channel.tsx | 6 +- web/components/issues/archive-issue-modal.tsx | 6 +- .../issues/attachment/attachment-detail.tsx | 8 +- .../issues/attachment/attachment-upload.tsx | 2 +- .../issues/attachment/attachments-list.tsx | 1 + .../delete-attachment-confirmation-modal.tsx | 2 +- web/components/issues/attachment/root.tsx | 2 +- web/components/issues/delete-issue-modal.tsx | 2 - web/components/issues/description-form.tsx | 16 +- web/components/issues/description-input.tsx | 9 +- .../issues/issue-detail/cycle-select.tsx | 7 +- .../issues/issue-detail/inbox/index.ts | 6 +- .../issue-detail/inbox/main-content.tsx | 12 +- .../issues/issue-detail/inbox/root.tsx | 16 +- .../issues/issue-detail/inbox/sidebar.tsx | 6 +- .../issue-activity/activity-comment-root.tsx | 1 + .../activity/actions/archived-at.tsx | 2 +- .../activity/actions/assignee.tsx | 4 +- .../issue-activity/activity/actions/cycle.tsx | 2 +- .../activity/actions/default.tsx | 2 +- .../activity/actions/estimate.tsx | 2 - .../actions/helpers/activity-block.tsx | 6 +- .../activity/actions/helpers/issue-link.tsx | 2 +- .../activity/actions/module.tsx | 2 +- .../activity/actions/relation.tsx | 4 +- .../activity/actions/start_date.tsx | 2 +- .../issue-activity/activity/actions/state.tsx | 2 +- .../activity/actions/target_date.tsx | 2 +- .../issue-activity/activity/root.tsx | 1 + .../issue-activity/comments/comment-block.tsx | 4 +- .../issue-activity/comments/comment-card.tsx | 14 +- .../comments/comment-create.tsx | 12 +- .../issue-activity/comments/root.tsx | 3 +- .../issue-detail/issue-activity/root.tsx | 4 +- .../issue-detail/label/create-label.tsx | 12 +- .../issue-detail/label/label-list-item.tsx | 2 +- .../issues/issue-detail/label/label-list.tsx | 3 +- .../issues/issue-detail/label/root.tsx | 4 +- .../label/select/label-select.tsx | 10 +- .../issues/issue-detail/label/select/root.tsx | 2 +- .../links/create-update-link-modal.tsx | 4 +- .../issues/issue-detail/links/link-detail.tsx | 8 +- .../issues/issue-detail/links/links.tsx | 3 +- .../issues/issue-detail/links/root.tsx | 4 +- .../issues/issue-detail/main-content.tsx | 10 +- .../issues/issue-detail/module-select.tsx | 9 +- .../issues/issue-detail/parent-select.tsx | 6 +- .../issues/issue-detail/parent/root.tsx | 4 +- .../issues/issue-detail/parent/siblings.tsx | 6 +- .../issue-detail/reactions/issue-comment.tsx | 17 +- .../issues/issue-detail/reactions/issue.tsx | 7 +- .../reactions/reaction-selector.tsx | 2 +- .../issues/issue-detail/relation-select.tsx | 18 +- web/components/issues/issue-detail/root.tsx | 29 +- .../issues/issue-detail/sidebar.tsx | 132 +-- .../issues/issue-detail/subscription.tsx | 6 +- .../calendar/base-calendar-root.tsx | 16 +- .../issue-layouts/calendar/calendar.tsx | 10 +- .../issue-layouts/calendar/day-tile.tsx | 6 +- .../calendar/dropdowns/months-dropdown.tsx | 4 +- .../calendar/dropdowns/options-dropdown.tsx | 10 +- .../issues/issue-layouts/calendar/header.tsx | 2 +- .../issue-layouts/calendar/issue-blocks.tsx | 6 +- .../calendar/quick-add-issue-form.tsx | 10 +- .../calendar/roots/cycle-root.tsx | 8 +- .../calendar/roots/module-root.tsx | 8 +- .../calendar/roots/project-root.tsx | 10 +- .../calendar/roots/project-view-root.tsx | 8 +- .../issue-layouts/calendar/week-days.tsx | 4 +- .../empty-states/archived-issues.tsx | 10 +- .../issue-layouts/empty-states/cycle.tsx | 16 +- .../empty-states/draft-issues.tsx | 10 +- .../empty-states/global-view.tsx | 2 +- .../issue-layouts/empty-states/module.tsx | 14 +- .../empty-states/project-issues.tsx | 10 +- .../empty-states/project-view.tsx | 4 +- .../filters/applied-filters/cycle.tsx | 2 +- .../filters/applied-filters/date.tsx | 2 +- .../filters/applied-filters/filters-list.tsx | 9 +- .../filters/applied-filters/module.tsx | 2 +- .../filters/applied-filters/priority.tsx | 2 +- .../filters/applied-filters/project.tsx | 2 +- .../applied-filters/roots/archived-issue.tsx | 6 +- .../applied-filters/roots/cycle-root.tsx | 6 +- .../applied-filters/roots/draft-issue.tsx | 6 +- .../roots/global-view-root.tsx | 10 +- .../applied-filters/roots/module-root.tsx | 6 +- .../roots/profile-issues-root.tsx | 6 +- .../applied-filters/roots/project-root.tsx | 6 +- .../roots/project-view-root.tsx | 10 +- .../filters/applied-filters/state-group.tsx | 2 +- .../filters/applied-filters/state.tsx | 2 +- .../display-filters-selection.tsx | 4 +- .../display-filters/display-properties.tsx | 4 +- .../header/display-filters/extra-options.tsx | 2 +- .../header/display-filters/group-by.tsx | 2 +- .../header/display-filters/issue-type.tsx | 2 +- .../header/display-filters/order-by.tsx | 2 +- .../header/display-filters/sub-group-by.tsx | 2 +- .../filters/header/filters/assignee.tsx | 8 +- .../filters/header/filters/created-by.tsx | 8 +- .../filters/header/filters/cycle.tsx | 4 +- .../header/filters/filters-selection.tsx | 6 +- .../filters/header/filters/labels.tsx | 2 +- .../filters/header/filters/mentions.tsx | 8 +- .../filters/header/filters/module.tsx | 4 +- .../filters/header/filters/project.tsx | 4 +- .../filters/header/filters/start-date.tsx | 2 +- .../filters/header/filters/state-group.tsx | 2 +- .../filters/header/filters/state.tsx | 2 +- .../filters/header/filters/target-date.tsx | 2 +- .../filters/header/helpers/dropdown.tsx | 40 +- .../filters/header/layout-selection.tsx | 2 +- .../issue-layouts/gantt/base-gantt-root.tsx | 12 +- .../issues/issue-layouts/gantt/blocks.tsx | 2 +- .../issues/issue-layouts/gantt/cycle-root.tsx | 6 +- .../issue-layouts/gantt/module-root.tsx | 6 +- .../issue-layouts/gantt/project-root.tsx | 8 +- .../issue-layouts/gantt/project-view-root.tsx | 8 +- .../gantt/quick-add-issue-form.tsx | 11 +- .../issue-layouts/kanban/base-kanban-root.tsx | 39 +- .../issues/issue-layouts/kanban/block.tsx | 12 +- .../issue-layouts/kanban/blocks-list.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 34 +- .../kanban/headers/group-by-card.tsx | 14 +- .../kanban/headers/sub-group-by-card.tsx | 2 +- .../issue-layouts/kanban/kanban-group.tsx | 2 +- .../kanban/quick-add-issue-form.tsx | 10 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 8 +- .../kanban/roots/draft-issue-root.tsx | 10 +- .../kanban/roots/module-root.tsx | 8 +- .../kanban/roots/profile-issues-root.tsx | 12 +- .../kanban/roots/project-root.tsx | 12 +- .../kanban/roots/project-view-root.tsx | 6 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 24 +- .../issue-layouts/list/base-list-root.tsx | 16 +- .../issues/issue-layouts/list/block.tsx | 4 +- .../issues/issue-layouts/list/blocks-list.tsx | 2 +- .../issues/issue-layouts/list/default.tsx | 12 +- .../list/headers/group-by-card.tsx | 10 +- .../list/quick-add-issue-form.tsx | 10 +- .../list/roots/archived-issue-root.tsx | 8 +- .../issue-layouts/list/roots/cycle-root.tsx | 8 +- .../list/roots/draft-issue-root.tsx | 6 +- .../issue-layouts/list/roots/module-root.tsx | 6 +- .../list/roots/profile-issues-root.tsx | 8 +- .../issue-layouts/list/roots/project-root.tsx | 6 +- .../list/roots/project-view-root.tsx | 6 +- .../properties/all-properties.tsx | 22 +- .../issue-layouts/properties/labels.tsx | 10 +- .../with-display-properties-HOC.tsx | 2 +- .../quick-action-dropdowns/all-issue.tsx | 17 +- .../quick-action-dropdowns/archived-issue.tsx | 14 +- .../quick-action-dropdowns/cycle-issue.tsx | 15 +- .../quick-action-dropdowns/module-issue.tsx | 15 +- .../quick-action-dropdowns/project-issue.tsx | 16 +- .../roots/all-issue-layout-root.tsx | 34 +- .../roots/archived-issue-layout-root.tsx | 6 +- .../issue-layouts/roots/cycle-layout-root.tsx | 10 +- .../roots/draft-issue-layout-root.tsx | 12 +- .../roots/module-layout-root.tsx | 6 +- .../roots/project-layout-root.tsx | 8 +- .../roots/project-view-layout-root.tsx | 4 +- .../spreadsheet/base-spreadsheet-root.tsx | 16 +- .../spreadsheet/columns/cycle-column.tsx | 6 +- .../spreadsheet/columns/due-date-column.tsx | 4 +- .../spreadsheet/columns/estimate-column.tsx | 2 +- .../spreadsheet/columns/header-column.tsx | 4 +- .../spreadsheet/columns/label-column.tsx | 2 +- .../spreadsheet/columns/module-column.tsx | 10 +- .../spreadsheet/columns/priority-column.tsx | 2 +- .../spreadsheet/columns/sub-issue-column.tsx | 2 +- .../spreadsheet/issue-column.tsx | 8 +- .../issue-layouts/spreadsheet/issue-row.tsx | 22 +- .../spreadsheet/quick-add-issue-form.tsx | 8 +- .../spreadsheet/roots/cycle-root.tsx | 6 +- .../spreadsheet/roots/module-root.tsx | 6 +- .../spreadsheet/roots/project-root.tsx | 6 +- .../spreadsheet/roots/project-view-root.tsx | 8 +- .../spreadsheet/spreadsheet-header-column.tsx | 2 +- .../spreadsheet/spreadsheet-header.tsx | 3 +- .../spreadsheet/spreadsheet-table.tsx | 18 +- .../spreadsheet/spreadsheet-view.tsx | 6 +- web/components/issues/issue-layouts/utils.tsx | 36 +- .../issues/issue-modal/draft-issue-layout.tsx | 8 +- web/components/issues/issue-modal/form.tsx | 38 +- web/components/issues/issue-modal/modal.tsx | 14 +- web/components/issues/issue-update-status.tsx | 2 +- .../issues/issues-mobile-header.tsx | 29 +- .../issues/parent-issues-list-modal.tsx | 8 +- .../issues/peek-overview/header.tsx | 10 +- .../issues/peek-overview/issue-detail.tsx | 4 +- .../issues/peek-overview/properties.tsx | 6 +- web/components/issues/peek-overview/root.tsx | 15 +- web/components/issues/peek-overview/view.tsx | 20 +- web/components/issues/select/label.tsx | 8 +- .../issues/sub-issues/issue-list-item.tsx | 8 +- .../issues/sub-issues/issues-list.tsx | 2 +- .../issues/sub-issues/properties.tsx | 2 +- web/components/issues/sub-issues/root.tsx | 12 +- web/components/issues/title-input.tsx | 2 +- web/components/labels/create-label-modal.tsx | 8 +- .../labels/create-update-label-inline.tsx | 11 +- web/components/labels/delete-label-modal.tsx | 6 +- .../labels/label-block/label-item-block.tsx | 4 +- .../labels/project-setting-label-group.tsx | 22 +- .../labels/project-setting-label-item.tsx | 4 +- .../labels/project-setting-label-list.tsx | 42 +- .../modules/delete-module-modal.tsx | 8 +- web/components/modules/form.tsx | 4 +- web/components/modules/gantt-chart/blocks.tsx | 6 +- .../gantt-chart/modules-list-layout.tsx | 4 +- web/components/modules/modal.tsx | 14 +- web/components/modules/module-card-item.tsx | 16 +- web/components/modules/module-list-item.tsx | 34 +- .../modules/module-mobile-header.tsx | 31 +- .../modules/module-peek-overview.tsx | 2 +- web/components/modules/modules-list-view.tsx | 10 +- web/components/modules/select/status.tsx | 2 +- .../modules/sidebar-select/select-status.tsx | 2 +- web/components/modules/sidebar.tsx | 32 +- .../notifications/notification-card.tsx | 33 +- .../notifications/notification-header.tsx | 18 +- .../notifications/notification-popover.tsx | 14 +- .../select-snooze-till-modal.tsx | 18 +- web/components/onboarding/invitations.tsx | 23 +- web/components/onboarding/invite-members.tsx | 28 +- web/components/onboarding/join-workspaces.tsx | 4 +- .../onboarding/onboarding-sidebar.tsx | 9 +- .../switch-delete-account-modal.tsx | 4 +- web/components/onboarding/tour/root.tsx | 14 +- web/components/onboarding/tour/sidebar.tsx | 2 +- web/components/onboarding/user-details.tsx | 24 +- web/components/onboarding/workspace.tsx | 7 +- web/components/page-views/signin.tsx | 6 +- .../page-views/workspace-dashboard.tsx | 16 +- .../pages/create-update-page-modal.tsx | 8 +- web/components/pages/delete-page-modal.tsx | 10 +- web/components/pages/page-form.tsx | 2 +- .../pages/pages-list/all-pages-list.tsx | 2 +- .../pages/pages-list/archived-pages-list.tsx | 2 +- .../pages/pages-list/favorite-pages-list.tsx | 2 +- web/components/pages/pages-list/list-item.tsx | 10 +- web/components/pages/pages-list/list-view.tsx | 8 +- .../pages/pages-list/private-page-list.tsx | 2 +- .../pages/pages-list/recent-pages-list.tsx | 12 +- .../pages/pages-list/shared-pages-list.tsx | 2 +- .../profile/activity/activity-list.tsx | 6 +- .../profile/activity/download-button.tsx | 2 +- .../activity/profile-activity-list.tsx | 12 +- .../activity/workspace-activity-list.tsx | 2 +- web/components/profile/navbar.tsx | 2 +- web/components/profile/overview/activity.tsx | 14 +- .../overview/priority-distribution.tsx | 88 ++ .../priority-distribution.tsx | 2 +- .../profile/overview/state-distribution.tsx | 2 +- web/components/profile/overview/stats.tsx | 4 +- web/components/profile/overview/workload.tsx | 2 +- web/components/profile/preferences/index.ts | 2 +- .../profile/profile-issues-filter.tsx | 2 +- web/components/profile/profile-issues.tsx | 20 +- web/components/profile/sidebar.tsx | 29 +- web/components/project/card-list.tsx | 6 +- web/components/project/card.tsx | 15 +- .../project/confirm-project-member-remove.tsx | 8 +- .../project/create-project-modal.tsx | 17 +- .../project/delete-project-modal.tsx | 6 +- web/components/project/form.tsx | 25 +- web/components/project/integration-card.tsx | 15 +- web/components/project/join-project-modal.tsx | 2 +- .../project/leave-project-modal.tsx | 12 +- web/components/project/member-list-item.tsx | 20 +- web/components/project/member-list.tsx | 4 +- web/components/project/member-select.tsx | 2 +- .../project-settings-member-defaults.tsx | 19 +- .../project/publish-project/modal.tsx | 20 +- .../project/send-project-invitation-modal.tsx | 8 +- .../settings/delete-project-section.tsx | 2 +- .../project/settings/features-list.tsx | 16 +- web/components/project/sidebar-list-item.tsx | 21 +- web/components/project/sidebar-list.tsx | 22 +- web/components/states/create-state-modal.tsx | 16 +- .../states/create-update-state-inline.tsx | 14 +- web/components/states/delete-state-modal.tsx | 10 +- .../project-setting-state-list-item.tsx | 6 +- .../states/project-setting-state-list.tsx | 14 +- web/components/toast-alert/index.tsx | 61 ++ web/components/ui/empty-space.tsx | 2 +- web/components/ui/graphs/bar-graph.tsx | 2 +- web/components/ui/graphs/calendar-graph.tsx | 2 +- web/components/ui/graphs/line-graph.tsx | 2 +- web/components/ui/graphs/marimekko-graph.tsx | 48 ++ web/components/ui/graphs/pie-graph.tsx | 2 +- .../ui/graphs/scatter-plot-graph.tsx | 2 +- .../ui/loader/cycle-module-board-loader.tsx | 7 +- .../ui/loader/cycle-module-list-loader.tsx | 7 +- .../project-inbox/inbox-layout-loader.tsx | 2 +- .../project-inbox/inbox-sidebar-loader.tsx | 4 +- .../ui/loader/notification-loader.tsx | 4 +- web/components/ui/loader/pages-loader.tsx | 8 +- web/components/ui/loader/projects-loader.tsx | 7 +- .../ui/loader/settings/activity.tsx | 4 +- .../ui/loader/settings/api-token.tsx | 4 +- web/components/ui/loader/settings/email.tsx | 4 +- .../ui/loader/settings/import-and-export.tsx | 4 +- .../ui/loader/settings/integration.tsx | 7 +- web/components/ui/loader/settings/members.tsx | 4 +- web/components/ui/loader/view-list-loader.tsx | 4 +- web/components/ui/multi-level-dropdown.tsx | 8 +- web/components/views/delete-view-modal.tsx | 6 +- web/components/views/form.tsx | 10 +- web/components/views/modal.tsx | 4 +- web/components/views/view-list-item.tsx | 14 +- web/components/views/views-list.tsx | 10 +- .../web-hooks/create-webhook-modal.tsx | 12 +- .../web-hooks/delete-webhook-modal.tsx | 4 +- web/components/web-hooks/form/form.tsx | 10 +- web/components/web-hooks/form/secret-key.tsx | 17 +- .../web-hooks/generated-hook-details.tsx | 2 +- .../web-hooks/webhooks-list-item.tsx | 2 +- .../confirm-workspace-member-remove.tsx | 6 +- .../workspace/create-workspace-form.tsx | 15 +- .../workspace/delete-workspace-modal.tsx | 12 +- web/components/workspace/help-section.tsx | 72 +- .../send-workspace-invitation-modal.tsx | 8 +- .../settings/invitations-list-item.tsx | 10 +- .../workspace/settings/members-list-item.tsx | 13 +- .../workspace/settings/members-list.tsx | 9 +- .../workspace/settings/workspace-details.tsx | 20 +- web/components/workspace/sidebar-dropdown.tsx | 12 +- web/components/workspace/sidebar-menu.tsx | 14 +- .../workspace/sidebar-quick-action.tsx | 15 +- .../views/default-view-list-item.tsx | 4 +- .../workspace/views/delete-view-modal.tsx | 11 +- web/components/workspace/views/form.tsx | 16 +- web/components/workspace/views/header.tsx | 17 +- web/components/workspace/views/modal.tsx | 10 +- .../workspace/views/view-list-item.tsx | 15 +- web/components/workspace/views/views-list.tsx | 17 +- .../workspace-active-cycles-upgrade.tsx | 16 +- web/constants/cycle.ts | 2 - web/constants/dashboard.ts | 13 +- web/constants/event-tracker.ts | 20 +- web/constants/spreadsheet.ts | 6 +- web/constants/workspace.ts | 8 +- web/contexts/user-notification-context.tsx | 2 +- web/helpers/analytics.helper.ts | 26 +- web/helpers/calendar.helper.ts | 2 +- web/helpers/filter.helper.ts | 1 - web/helpers/issue.helper.ts | 12 +- web/helpers/string.helper.ts | 10 +- web/hooks/store/index.ts | 2 +- web/hooks/store/use-inbox-issues.ts | 2 +- web/hooks/store/use-issues.ts | 12 +- web/hooks/use-comment-reaction.tsx | 2 +- web/hooks/use-draggable-portal.ts | 2 +- web/hooks/use-dropdown-key-down.tsx | 8 +- web/hooks/use-user-notifications.tsx | 2 +- web/hooks/use-user.tsx | 2 +- web/layouts/admin-layout/header.tsx | 2 +- web/layouts/admin-layout/layout.tsx | 4 +- web/layouts/admin-layout/sidebar.tsx | 2 +- web/layouts/app-layout/layout.tsx | 7 +- web/layouts/app-layout/sidebar.tsx | 2 +- web/layouts/auth-layout/admin-wrapper.tsx | 2 +- web/layouts/auth-layout/project-wrapper.tsx | 10 +- web/layouts/auth-layout/user-wrapper.tsx | 4 +- web/layouts/auth-layout/workspace-wrapper.tsx | 6 +- web/layouts/instance-layout/index.tsx | 6 +- .../settings-layout/profile/layout.tsx | 2 +- .../profile/preferences/index.ts | 2 +- .../profile/preferences/layout.tsx | 10 +- .../profile/preferences/sidebar.tsx | 29 +- .../settings-layout/profile/sidebar.tsx | 6 +- .../settings-layout/project/layout.tsx | 8 +- .../settings-layout/project/sidebar.tsx | 4 +- .../settings-layout/workspace/sidebar.tsx | 4 +- web/layouts/user-profile-layout/layout.tsx | 3 +- web/lib/app-provider.tsx | 16 +- web/lib/local-storage.ts | 10 +- web/lib/posthog-provider.tsx | 10 +- web/lib/types.d.ts | 1 + web/lib/wrappers/crisp-wrapper.tsx | 8 +- web/lib/wrappers/store-wrapper.tsx | 10 +- web/package.json | 4 - web/pages/404.tsx | 6 +- web/pages/[workspaceSlug]/active-cycles.tsx | 2 +- web/pages/[workspaceSlug]/analytics.tsx | 14 +- web/pages/[workspaceSlug]/index.tsx | 6 +- .../profile/[userId]/activity.tsx | 10 +- .../profile/[userId]/assigned.tsx | 6 +- .../profile/[userId]/created.tsx | 6 +- .../profile/[userId]/index.tsx | 12 +- .../profile/[userId]/subscribed.tsx | 6 +- .../[projectId]/archived-issues/index.tsx | 10 +- .../projects/[projectId]/cycles/[cycleId].tsx | 14 +- .../projects/[projectId]/cycles/index.tsx | 24 +- .../[projectId]/draft-issues/index.tsx | 8 +- .../projects/[projectId]/inbox/[inboxId].tsx | 10 +- .../projects/[projectId]/inbox/index.tsx | 6 +- .../projects/[projectId]/issues/[issueId].tsx | 10 +- .../projects/[projectId]/issues/index.tsx | 10 +- .../[projectId]/modules/[moduleId].tsx | 14 +- .../projects/[projectId]/modules/index.tsx | 8 +- .../projects/[projectId]/pages/[pageId].tsx | 26 +- .../projects/[projectId]/pages/index.tsx | 26 +- .../[projectId]/settings/automations.tsx | 17 +- .../[projectId]/settings/estimates.tsx | 8 +- .../[projectId]/settings/features.tsx | 8 +- .../projects/[projectId]/settings/index.tsx | 12 +- .../[projectId]/settings/integrations.tsx | 20 +- .../projects/[projectId]/settings/labels.tsx | 8 +- .../projects/[projectId]/settings/members.tsx | 6 +- .../projects/[projectId]/settings/states.tsx | 8 +- .../projects/[projectId]/views/[viewId].tsx | 12 +- .../projects/[projectId]/views/index.tsx | 4 +- web/pages/[workspaceSlug]/projects/index.tsx | 4 +- .../[workspaceSlug]/settings/api-tokens.tsx | 24 +- .../[workspaceSlug]/settings/billing.tsx | 8 +- .../[workspaceSlug]/settings/exports.tsx | 8 +- .../[workspaceSlug]/settings/imports.tsx | 10 +- web/pages/[workspaceSlug]/settings/index.tsx | 8 +- .../[workspaceSlug]/settings/integrations.tsx | 16 +- .../[workspaceSlug]/settings/members.tsx | 18 +- .../settings/webhooks/[webhookId].tsx | 11 +- .../settings/webhooks/index.tsx | 18 +- .../workspace-views/[globalViewId].tsx | 18 +- .../[workspaceSlug]/workspace-views/index.tsx | 12 +- web/pages/_document.tsx | 2 +- web/pages/_error.tsx | 5 +- web/pages/accounts/sign-up.tsx | 10 +- web/pages/create-workspace.tsx | 14 +- web/pages/god-mode/ai.tsx | 12 +- web/pages/god-mode/authorization.tsx | 11 +- web/pages/god-mode/email.tsx | 10 +- web/pages/god-mode/image.tsx | 10 +- web/pages/god-mode/index.tsx | 10 +- web/pages/index.tsx | 2 +- web/pages/installations/[provider]/index.tsx | 2 +- web/pages/invitations/index.tsx | 3 +- web/pages/onboarding/index.tsx | 32 +- web/pages/profile/activity.tsx | 8 +- web/pages/profile/change-password.tsx | 14 +- web/pages/profile/index.tsx | 41 +- web/pages/profile/preferences/email.tsx | 8 +- web/pages/profile/preferences/theme.tsx | 14 +- web/pages/workspace-invitations/index.tsx | 10 +- web/services/ai.service.ts | 2 +- web/services/analytics.service.ts | 2 +- web/services/api_token.service.ts | 2 +- web/services/app_config.service.ts | 2 +- web/services/app_installation.service.ts | 2 +- web/services/auth.service.ts | 2 +- web/services/cycle.service.ts | 2 +- web/services/dashboard.service.ts | 2 +- web/services/file.service.ts | 4 +- web/services/inbox.service.ts | 2 +- web/services/inbox/inbox-issue.service.ts | 2 +- web/services/inbox/inbox.service.ts | 2 +- web/services/instance.service.ts | 2 +- web/services/integrations/github.service.ts | 2 +- .../integrations/integration.service.ts | 2 +- web/services/integrations/jira.service.ts | 2 +- web/services/issue/issue.service.ts | 2 +- web/services/issue/issue_activity.service.ts | 2 +- web/services/issue/issue_archive.service.ts | 2 +- .../issue/issue_attachment.service.ts | 2 +- web/services/issue/issue_comment.service.ts | 2 +- web/services/issue/issue_draft.service.ts | 2 +- web/services/issue_filter.service.ts | 2 +- web/services/module.service.ts | 2 +- web/services/notification.service.ts | 2 +- .../project/project-estimate.service.ts | 2 +- .../project/project-export.service.ts | 2 +- web/services/project/project-state.service.ts | 2 +- web/services/user.service.ts | 2 +- web/services/view.service.ts | 2 +- web/services/webhook.service.ts | 2 +- web/services/workspace.service.ts | 2 +- web/store/application/app-config.store.ts | 2 +- .../application/command-palette.store.ts | 4 +- web/store/application/index.ts | 2 +- web/store/application/instance.store.ts | 2 +- web/store/cycle.store.ts | 12 +- web/store/dashboard.store.ts | 2 +- web/store/estimate.store.ts | 4 +- web/store/event-tracker.store.ts | 2 +- web/store/global-view.store.ts | 2 +- web/store/inbox/inbox.store.ts | 8 +- web/store/inbox/inbox_filter.store.ts | 4 +- web/store/inbox/inbox_issue.store.ts | 10 +- web/store/inbox/root.store.ts | 2 +- web/store/issue/archived/filter.store.ts | 18 +- web/store/issue/archived/issue.store.ts | 8 +- web/store/issue/cycle/filter.store.ts | 18 +- web/store/issue/cycle/issue.store.ts | 12 +- web/store/issue/draft/filter.store.ts | 18 +- web/store/issue/draft/issue.store.ts | 12 +- .../helpers/issue-filter-helper.store.ts | 8 +- web/store/issue/helpers/issue-helper.store.ts | 6 +- .../issue/issue-details/activity.store.ts | 8 +- .../issue/issue-details/attachment.store.ts | 10 +- .../issue/issue-details/comment.store.ts | 10 +- .../issue-details/comment_reaction.store.ts | 12 +- web/store/issue/issue-details/issue.store.ts | 2 +- web/store/issue/issue-details/link.store.ts | 4 +- .../issue/issue-details/reaction.store.ts | 12 +- .../issue/issue-details/relation.store.ts | 4 +- web/store/issue/issue-details/root.store.ts | 30 +- .../issue/issue-details/sub_issues.store.ts | 8 +- .../issue/issue-details/subscription.store.ts | 2 +- web/store/issue/issue.store.ts | 4 +- web/store/issue/issue_calendar_view.store.ts | 2 +- web/store/issue/issue_gantt_view.store.ts | 2 +- web/store/issue/module/filter.store.ts | 18 +- web/store/issue/module/issue.store.ts | 10 +- web/store/issue/profile/filter.store.ts | 18 +- web/store/issue/profile/issue.store.ts | 8 +- web/store/issue/project-views/filter.store.ts | 18 +- web/store/issue/project-views/issue.store.ts | 8 +- web/store/issue/project/filter.store.ts | 19 +- web/store/issue/project/issue.store.ts | 10 +- web/store/issue/root.store.ts | 22 +- web/store/issue/workspace/filter.store.ts | 18 +- web/store/issue/workspace/issue.store.ts | 10 +- web/store/label.store.ts | 6 +- web/store/member/index.ts | 2 +- web/store/member/project-member.store.ts | 10 +- web/store/member/workspace-member.store.ts | 10 +- web/store/mention.store.ts | 2 +- web/store/module.store.ts | 8 +- web/store/page.store.ts | 2 +- web/store/project-page.store.ts | 4 +- web/store/project/index.ts | 4 +- web/store/project/project-publish.store.ts | 4 +- web/store/project/project.store.ts | 12 +- web/store/root.store.ts | 22 +- web/store/state.store.ts | 10 +- web/store/user/index.ts | 2 +- web/store/user/user-membership.store.ts | 6 +- web/store/workspace/api-token.store.ts | 2 +- web/store/workspace/index.ts | 8 +- web/store/workspace/webhook.store.ts | 2 +- yarn.lock | 768 +++++------------- 790 files changed, 4079 insertions(+), 3975 deletions(-) create mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx create mode 100644 web/components/profile/overview/priority-distribution.tsx create mode 100644 web/components/toast-alert/index.tsx create mode 100644 web/components/ui/graphs/marimekko-graph.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 148568d76fb..f40c1a244bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,6 @@ chmod +x setup.sh docker compose -f docker-compose-local.yml up ``` - ## Missing a Feature? If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. diff --git a/ENV_SETUP.md b/ENV_SETUP.md index bfc30019624..df05683efd9 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -53,7 +53,6 @@ NGINX_PORT=80 NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" ``` - ## {PROJECT_FOLDER}/apiserver/.env ​ diff --git a/README.md b/README.md index 52ccda474f1..6834199ffe5 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,18 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. - - ## ⚡ Installation The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users. If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). -| Installation Methods | Documentation Link | -|-----------------|----------------------------------------------------------------------------------------------------------| -| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | -| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | +| Installation Methods | Documentation Link | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | +| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | -`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. +`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. ## 🚀 Features @@ -74,9 +72,7 @@ If you want more control over your data prefer to self-host Plane, please refer - **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work. -- **Drive** (*coming soon*): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. - - +- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. ## 🛠️ Contributors Quick Start @@ -101,10 +97,10 @@ Setting up local environment is extremely easy and straight forward. Follow the ./setup.sh ``` 5. Open the code on VSCode or similar equivalent IDE. -6. Review the `.env` files available in various folders. +6. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system. 7. Run the docker command to initiate services: - ``` + ``` docker compose -f docker-compose-local.yml up -d ``` @@ -119,6 +115,7 @@ The Plane community can be found on [GitHub Discussions](https://github.com/orgs Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects. ### Repo Activity + ![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image") ## 📸 Screenshots @@ -181,20 +178,21 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests ## ⛓️ Security -If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. +If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email squawk@plane.so to disclose any security vulnerabilities. ## ❤️ Contribute -There are many ways to contribute to Plane, including: -- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. -- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. -- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! -- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. +There are many ways to contribute to Plane, including: + +- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. +- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. +- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! +- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. ### We couldn't have done this without you. - \ No newline at end of file + diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 88ea66c4c8e..08bc35b28fd 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -31,11 +31,11 @@ curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-cl ``` NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture + -- - Expect this after a successful install ![Install Output](images/install.png) @@ -50,29 +50,33 @@ Plane App is available via the command `plane-app`. Running the command `plane-a ![Plane Help](images/help.png) -Basic Operations: +Basic Operations: + 1. Start Server using `plane-app start` 1. Stop Server using `plane-app stop` 1. Restart Server using `plane-app restart` Advanced Operations: + 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) + + - 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. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) -1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. +1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. + +1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. -1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. +1. Plane App can be reinstalled using `plane-app --install`. -1. Plane App can be reinstalled using `plane-app --install`. +Application Data is stored in the mentioned folders: -Application Data is stored in the mentioned folders: 1. DB Data: /opt/plane/data/postgres 1. Redis Data: /opt/plane/data/redis -1. Minio Data: /opt/plane/data/minio \ No newline at end of file +1. Minio Data: /opt/plane/data/minio diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index fcb6b57bbb2..198b21b0f68 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -59,8 +59,7 @@ "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "^7.32.0", - "eslint-config-next": "13.2.4", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json index bd1f2d90fea..870d5edd9e1 100644 --- a/packages/editor/document-editor/package.json +++ b/packages/editor/document-editor/package.json @@ -37,7 +37,6 @@ "@tiptap/extension-placeholder": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/suggestion": "^2.1.13", - "eslint-config-next": "13.2.4", "lucide-react": "^0.309.0", "react-popper": "^2.3.0", "tippy.js": "^6.3.7", @@ -47,7 +46,7 @@ "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "8.36.0", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/document-editor/src/ui/components/content-browser.tsx b/packages/editor/document-editor/src/ui/components/content-browser.tsx index 97231ea966f..be70067a202 100644 --- a/packages/editor/document-editor/src/ui/components/content-browser.tsx +++ b/packages/editor/document-editor/src/ui/components/content-browser.tsx @@ -15,7 +15,7 @@ export const ContentBrowser = (props: ContentBrowserProps) => { const handleOnClick = (marking: IMarking) => { scrollSummary(editor, marking); if (setSidePeekVisible) setSidePeekVisible(false); - } + }; return (
    diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx index 6ad7cad835d..41056c6ad26 100644 --- a/packages/editor/document-editor/src/ui/components/summary-popover.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -33,8 +33,9 @@ export const SummaryPopover: React.FC = (props) => { )} - {as_ === "link" && {display}} + {itemAs === "link" && {display}} {children && setOpen(false)} items={children} />}
    diff --git a/space/lib/mobx/store-provider.tsx b/space/lib/mobx/store-provider.tsx index c6fde14ae02..e12f2823ae9 100644 --- a/space/lib/mobx/store-provider.tsx +++ b/space/lib/mobx/store-provider.tsx @@ -9,10 +9,10 @@ let rootStore: RootStore = new RootStore(); export const MobxStoreContext = createContext(rootStore); const initializeStore = () => { - const _rootStore: RootStore = rootStore ?? new RootStore(); - if (typeof window === "undefined") return _rootStore; - if (!rootStore) rootStore = _rootStore; - return _rootStore; + const singletonRootStore: RootStore = rootStore ?? new RootStore(); + if (typeof window === "undefined") return singletonRootStore; + if (!rootStore) rootStore = singletonRootStore; + return singletonRootStore; }; export const MobxStoreProvider = ({ children }: any) => { diff --git a/space/package.json b/space/package.json index a1d600a60bf..7018cd24165 100644 --- a/space/package.json +++ b/space/package.json @@ -49,9 +49,7 @@ "@types/react-dom": "^18.2.17", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", - "eslint": "8.34.0", "eslint-config-custom": "*", - "eslint-config-next": "13.2.1", "tailwind-config-custom": "*", "tsconfig": "*" } diff --git a/web/.eslintrc.js b/web/.eslintrc.js index c8df607506c..eb05b2af8d7 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,4 +1,103 @@ module.exports = { root: true, extends: ["custom"], + parser: "@typescript-eslint/parser", + settings: { + "import/resolver": { + typescript: {}, + node: { + moduleDirectory: ["node_modules", "."], + }, + }, + }, + rules: { + // "import/order": [ + // "error", + // { + // groups: ["builtin", "external", "internal", "parent", "sibling"], + // pathGroups: [ + // { + // pattern: "react", + // group: "external", + // position: "before", + // }, + // { + // pattern: "@headlessui/**", + // group: "external", + // position: "after", + // }, + // { + // pattern: "lucide-react", + // group: "external", + // position: "after", + // }, + // { + // pattern: "@plane/ui", + // group: "external", + // position: "after", + // }, + // { + // pattern: "components/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "constants/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "contexts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "helpers/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "hooks/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "layouts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "lib/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "services/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "store/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "@plane/types", + // group: "internal", + // position: "after", + // }, + // { + // pattern: "lib/types", + // group: "internal", + // position: "after", + // }, + // ], + // pathGroupsExcludedImportTypes: ["builtin", "internal", "react"], + // alphabetize: { + // order: "asc", + // caseInsensitive: true, + // }, + // }, + // ], + }, }; diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 41d1fd7ca4e..34129cebec4 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; -import { mutate } from "swr"; // hooks -import { useUser } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -86,9 +86,9 @@ export const DeactivateAccountModal: React.FC = (props) => {
    -
    +
    diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index bbb73b855d0..1671b94fcfa 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -1,12 +1,10 @@ import { observer } from "mobx-react-lite"; // services -import { AuthService } from "services/auth.service"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { GitHubSignInButton, GoogleSignInButton } from "components/account"; import { useApplication } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { GitHubSignInButton, GoogleSignInButton } from "components/account"; type Props = { handleSignInRedirection: () => Promise; diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index 8fc7935cdec..5555d001649 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -157,7 +157,7 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => {
    )} /> -

    +

    Whatever you choose now will be your account{"'"}s password until you change it.

    diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index 7d51b0cf557..f42398850e2 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -1,22 +1,22 @@ import React, { useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { ESignInSteps, ForgotPasswordPopover } from "components/account"; +import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useApplication, useEventTracker } from "hooks/store"; import { AuthService } from "services/auth.service"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; // components -import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IPasswordSignInData } from "@plane/types"; // constants -import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index 62f63caea69..835e018dc19 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,11 +1,7 @@ import React, { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; -import useSignInRedirection from "hooks/use-sign-in-redirection"; -// components -import { LatestFeatureBlock } from "components/common"; import { SignInEmailForm, SignInUniqueCodeForm, @@ -13,8 +9,12 @@ import { OAuthOptions, SignInOptionalSetPasswordForm, } from "components/account"; -// constants +import { LatestFeatureBlock } from "components/common"; import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker"; +import { useApplication, useEventTracker } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +// constants export enum ESignInSteps { EMAIL = "EMAIL", diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 25ee4c462b2..6929ef0febe 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -2,19 +2,21 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; + +import { CODE_VERIFIED } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; + +import useTimer from "hooks/use-timer"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useTimer from "hooks/use-timer"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants -import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/account/sign-up-forms/email.tsx b/web/components/account/sign-up-forms/email.tsx index b65ca95bf59..22dba892fae 100644 --- a/web/components/account/sign-up-forms/email.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -1,13 +1,13 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; -import { observer } from "mobx-react-lite"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { checkEmailValidity } from "helpers/string.helper"; import { AuthService } from "services/auth.service"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData } from "@plane/types"; diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index 651f2815ffd..93f77424859 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -1,19 +1,19 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; // services +import { Eye, EyeOff } from "lucide-react"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { ESignUpSteps } from "components/account"; +import { PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; import { AuthService } from "services/auth.service"; // hooks -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // components -import { ESignUpSteps } from "components/account"; // constants -import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons -import { Eye, EyeOff } from "lucide-react"; type Props = { email: string; @@ -162,7 +162,7 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => {
    )} /> -

    +

    This password will continue to be your account{"'"}s password.

    diff --git a/web/components/account/sign-up-forms/password.tsx b/web/components/account/sign-up-forms/password.tsx index 5207a50243b..7fab81fbe1a 100644 --- a/web/components/account/sign-up-forms/password.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services -import { AuthService } from "services/auth.service"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; +import { AuthService } from "services/auth.service"; // types import { IPasswordSignInData } from "@plane/types"; @@ -134,7 +134,7 @@ export const SignUpPasswordForm: React.FC = observer((props) => {
    )} /> -

    +

    This password will continue to be your account{"'"}s password.

    diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx index 8eeb5e99f96..455112e9ede 100644 --- a/web/components/account/sign-up-forms/root.tsx +++ b/web/components/account/sign-up-forms/root.tsx @@ -1,9 +1,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; -import useSignInRedirection from "hooks/use-sign-in-redirection"; -// components +import Link from "next/link"; import { OAuthOptions, SignUpEmailForm, @@ -11,9 +9,11 @@ import { SignUpPasswordForm, SignUpUniqueCodeForm, } from "components/account"; -import Link from "next/link"; -// constants import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker"; +import { useApplication, useEventTracker } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +// constants export enum ESignUpSteps { EMAIL = "EMAIL", diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 51705ea6782..28581aed469 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -3,19 +3,20 @@ import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; + +import { CODE_VERIFIED } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; +import useTimer from "hooks/use-timer"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useTimer from "hooks/use-timer"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants -import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index 0c3ec89250b..1159689c6c0 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -1,17 +1,17 @@ +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // components import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; // types -import { IAnalyticsParams } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams } from "@plane/types"; type Props = { additionalParams?: Partial; diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index ec7c4019507..b90e9994fcb 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -60,8 +60,8 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => ? "capitalize" : "" : params.x_axis === "priority" || params.x_axis === "state__group" - ? "capitalize" - : "" + ? "capitalize" + : "" }`} > {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 51b4089c4f2..0e70fd8984b 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -1,15 +1,15 @@ // nivo import { BarDatum } from "@nivo/bar"; // components -import { CustomTooltip } from "./custom-tooltip"; import { Tooltip } from "@plane/ui"; // ui import { BarGraph } from "components/ui"; // helpers -import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; +import { findStringWithMostCharacters } from "helpers/array.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; +import { CustomTooltip } from "./custom-tooltip"; type Props = { analytics: IAnalyticsResponse; @@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() : "?" : datum.value && datum.value !== "None" - ? `${datum.value}`.toUpperCase()[0] - : "?"} + ? `${datum.value}`.toUpperCase()[0] + : "?"} diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 7781e786962..e13b9cdd1d2 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -2,15 +2,15 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; // components +import { Button, Loader } from "@plane/ui"; import { AnalyticsGraph, AnalyticsTable } from "components/analytics"; // ui -import { Button, Loader } from "@plane/ui"; // helpers +import { ANALYTICS } from "constants/fetch-keys"; import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; // fetch-keys -import { ANALYTICS } from "constants/fetch-keys"; type Props = { analytics: IAnalyticsResponse | undefined; diff --git a/web/components/analytics/custom-analytics/select-bar.tsx b/web/components/analytics/custom-analytics/select-bar.tsx index 31acb84718c..7ce2f31ef7b 100644 --- a/web/components/analytics/custom-analytics/select-bar.tsx +++ b/web/components/analytics/custom-analytics/select-bar.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; // hooks +import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; import { useProject } from "hooks/store"; // components -import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; // types import { IAnalyticsParams } from "@plane/types"; @@ -22,8 +22,9 @@ export const CustomAnalyticsSelectBar: React.FC = observer((props) => { return (
    {!isProjectLevel && (
    diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index 3c08e157473..61c3acb09a2 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { CustomSearchSelect } from "@plane/ui"; import { useProject } from "hooks/store"; // ui -import { CustomSearchSelect } from "@plane/ui"; type Props = { value: string[] | undefined; diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index 055665d9ee2..de94eac6223 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues | null | undefined; diff --git a/web/components/analytics/custom-analytics/select/x-axis.tsx b/web/components/analytics/custom-analytics/select/x-axis.tsx index 74ee99a7708..9daecaaa0ef 100644 --- a/web/components/analytics/custom-analytics/select/x-axis.tsx +++ b/web/components/analytics/custom-analytics/select/x-axis.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues; diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index 9f66c6b5450..92e4fd2e539 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,9 +1,9 @@ // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { TYAxisValues } from "@plane/types"; // constants -import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; type Props = { value: TYAxisValues; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 334e9160f16..9a0eec22751 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; +import { useProject } from "hooks/store"; type Props = { projectIds: string[]; diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index 6a7b3c7b9d0..fb9ab90fa3d 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { NETWORK_CHOICES } from "constants/project"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useCycle, useMember, useModule, useProject } from "hooks/store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; // constants -import { NETWORK_CHOICES } from "constants/project"; export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index aab7f874f28..7a7c5237753 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,24 +1,24 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { mutate } from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // hooks -import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; // components -import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui +import { CalendarDays, Download, RefreshCw } from "lucide-react"; import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { CalendarDays, Download, RefreshCw } from "lucide-react"; +import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; type Props = { analytics: IAnalyticsResponse | undefined; @@ -143,7 +143,7 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { return (
    @@ -176,10 +176,10 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => {
    -
    +
    diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 3d0fbb9ee11..7f41b822547 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -3,15 +3,16 @@ import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleDelete?: () => void; diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index eec62b91909..9c1a8363bfb 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleRemove?: () => void; diff --git a/web/components/core/render-if-visible-HOC.tsx b/web/components/core/render-if-visible-HOC.tsx index 24ae19fe79b..f0e9f59b4a4 100644 --- a/web/components/core/render-if-visible-HOC.tsx +++ b/web/components/core/render-if-visible-HOC.tsx @@ -1,10 +1,10 @@ -import { cn } from "helpers/common.helper"; import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react"; +import { cn } from "helpers/common.helper"; type Props = { defaultHeight?: string; verticalOffset?: number; - horizonatlOffset?: number; + horizontalOffset?: number; root?: MutableRefObject; children: ReactNode; as?: keyof JSX.IntrinsicElements; @@ -20,7 +20,7 @@ const RenderIfVisible: React.FC = (props) => { defaultHeight = "300px", root, verticalOffset = 50, - horizonatlOffset = 0, + horizontalOffset = 0, as = "div", children, classNames = "", @@ -52,17 +52,18 @@ const RenderIfVisible: React.FC = (props) => { }, { root: root?.current, - rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`, + rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`, } ); observer.observe(intersectionRef.current); return () => { if (intersectionRef.current) { + // eslint-disable-next-line react-hooks/exhaustive-deps observer.unobserve(intersectionRef.current); } }; } - }, [root?.current, intersectionRef, children, changingReference]); + }, [intersectionRef, children, changingReference, root, verticalOffset, horizontalOffset]); //Set height after render useEffect(() => { diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 6b987e30833..3e068e4f07c 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -1,15 +1,14 @@ -// ui -import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { observer } from "mobx-react"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; +// ui +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; -// types -import { ILinkDetails, UserAuth } from "@plane/types"; // hooks -import { observer } from "mobx-react"; -import { useMeasure } from "@nivo/core"; import { useMember } from "hooks/store"; +// types +import { ILinkDetails, UserAuth } from "@plane/types"; type Props = { links: ILinkDetails[]; diff --git a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx index 0212e49802a..880cf81463d 100644 --- a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx +++ b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; +import { observer } from "mobx-react"; import { Menu } from "lucide-react"; import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; type Props = { onClick?: () => void; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 12c387f471e..157fd2c7918 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -4,14 +4,14 @@ import Image from "next/image"; // headless ui import { Tab } from "@headlessui/react"; // hooks +import { Avatar, StateGroupIcon } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // images import emptyLabel from "public/empty-state/empty_label.svg"; import emptyMembers from "public/empty-state/empty_members.svg"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar, StateGroupIcon } from "@plane/ui"; // types import { IModule, diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index 19cd519cbff..03ac06eae60 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -1,5 +1,6 @@ import { FC, Fragment } from "react"; // react-form +import { ColorResult, SketchPicker } from "react-color"; import { Control, Controller, @@ -11,12 +12,11 @@ import { UseFormWatch, } from "react-hook-form"; // react-color -import { ColorResult, SketchPicker } from "react-color"; // component import { Popover, Transition } from "@headlessui/react"; +import { Palette } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Palette } from "lucide-react"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index fdb7a648347..b9e94a2d2ca 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; import { useTheme } from "next-themes"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, InputColorPicker } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button, InputColorPicker } from "@plane/ui"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/theme-switch.tsx b/web/components/core/theme/theme-switch.tsx index bcd847a280a..428e6930ba0 100644 --- a/web/components/core/theme/theme-switch.tsx +++ b/web/components/core/theme/theme-switch.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // constants +import { CustomSelect } from "@plane/ui"; import { THEME_OPTIONS, I_THEME_OPTION } from "constants/themes"; // ui -import { CustomSelect } from "@plane/ui"; type Props = { value: I_THEME_OPTION | null; diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index bc22cb8ab12..d9309d4b529 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,8 +1,8 @@ import { MouseEvent } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import Link from "next/link"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; // ui @@ -183,7 +183,7 @@ export const ActiveCycleDetails: React.FC = observer((props - + {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} {activeCycle.is_favorite ? ( @@ -303,9 +303,9 @@ export const ActiveCycleDetails: React.FC = observer((props
    -
    +
    High Priority Issues
    -
    +
    {activeCycleIssues ? ( activeCycleIssues.length > 0 ? ( activeCycleIssues.map((issue: any) => ( @@ -329,17 +329,17 @@ export const ActiveCycleDetails: React.FC = observer((props {truncateText(issue.name, 30)}
    -
    +
    {}} projectId={projectId?.toString() ?? ""} - disabled={true} + disabled buttonVariant="background-with-text" /> {issue.target_date && ( -
    +
    {renderFormattedDateWithoutYear(issue.target_date)}
    @@ -349,7 +349,7 @@ export const ActiveCycleDetails: React.FC = observer((props )) ) : ( -
    +
    There are no high priority issues present in this cycle.
    ) @@ -362,7 +362,7 @@ export const ActiveCycleDetails: React.FC = observer((props )}
    -
    +
    diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 3ca5caeb204..7d935c34779 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -1,11 +1,11 @@ import React, { Fragment } from "react"; import { Tab } from "@headlessui/react"; // hooks +import { Avatar } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar } from "@plane/ui"; // types import { ICycle } from "@plane/types"; diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index 624334ec473..942b5832b97 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -1,16 +1,16 @@ import { useCallback, useState } from "react"; import router from "next/router"; //components -import { CustomMenu } from "@plane/ui"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { CustomMenu } from "@plane/ui"; // hooks -import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; +import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const CycleMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); @@ -100,6 +100,7 @@ export const CycleMobileHeader = () => { > {layouts.map((layout, index) => ( { handleLayoutChange(ISSUE_LAYOUTS[index].key); }} diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fbfb46b50b5..b7e778c1012 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useCycle } from "hooks/store"; // components diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 72af8409df3..2eecb1ae901 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -1,12 +1,10 @@ import { FC, MouseEvent, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; // components -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; -// ui +import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; import { Avatar, AvatarGroup, @@ -18,15 +16,17 @@ import { setToast, setPromiseToast, } from "@plane/ui"; +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +// ui // icons -import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; //.types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 1a90692675d..00c98e57cb2 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -2,12 +2,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useUser } from "hooks/store"; export interface ICyclesBoard { cycleIds: string[]; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 9ab2e3de8ed..9bf1866ffc7 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -1,12 +1,9 @@ import { FC, MouseEvent, useState } from "react"; +import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -// components -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; -// ui +import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; import { CustomMenu, Tooltip, @@ -18,17 +15,20 @@ import { setToast, setPromiseToast, } from "@plane/ui"; -// icons -import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; -// helpers +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// components +// ui +// icons +// helpers // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; // types import { TCycleGroups } from "@plane/types"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; type TCyclesListItem = { cycleId: string; @@ -227,7 +227,7 @@ export const CyclesListItem: FC = observer((props) => {
    -
    diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 173a7f4b7cd..99cf1f2b1f0 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -2,14 +2,14 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useUser } from "hooks/store"; // components +import { Loader } from "@plane/ui"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Loader } from "@plane/ui"; // constants import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useUser } from "hooks/store"; export interface ICyclesList { cycleIds: string[]; diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index a321be0b592..745ca1bd362 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useCycle } from "hooks/store"; // components import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; // ui components import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +import { useCycle } from "hooks/store"; // types import { TCycleLayout, TCycleView } from "@plane/types"; @@ -32,10 +32,10 @@ export const CyclesView: FC = observer((props) => { filter === "completed" ? currentProjectCompletedCycleIds : filter === "draft" - ? currentProjectDraftCycleIds - : filter === "upcoming" - ? currentProjectUpcomingCycleIds - : currentProjectCycleIds; + ? currentProjectDraftCycleIds + : filter === "upcoming" + ? currentProjectUpcomingCycleIds + : currentProjectCycleIds; if (loader || !cyclesList) return ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 239fe6a663b..fd7b1f356ab 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,16 +1,16 @@ import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { CYCLE_DELETED } from "constants/event-tracker"; import { useEventTracker, useCycle } from "hooks/store"; // components -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ICycle } from "@plane/types"; // constants -import { CYCLE_DELETED } from "constants/event-tracker"; interface ICycleDelete { cycle: ICycle; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 799d8043828..4e2f55ef986 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,9 +1,9 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components +import { Button, Input, TextArea } from "@plane/ui"; import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 5d82c94a863..e9fdd50de00 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,11 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useCycle } from "hooks/store"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; +import { useApplication, useCycle } from "hooks/store"; type Props = { cycleId: string; @@ -33,12 +33,12 @@ export const CycleGanttBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "", + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "", }} onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)} > @@ -86,12 +86,12 @@ export const CycleGanttSidebarBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "" + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "" }`} />
    {cycleDetails?.name}
    diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 646333aad90..521273c5123 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,15 +1,15 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleGanttBlock } from "components/cycles"; +import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; +import { EUserProjectRoles } from "constants/project"; import { useCycle, useUser } from "hooks/store"; // components -import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; -import { CycleGanttBlock } from "components/cycles"; // types import { ICycle } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 1d60f1dc4fd..2d1640ec909 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -1,18 +1,18 @@ import React, { useEffect, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; // services -import { CycleService } from "services/cycle.service"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { CycleForm } from "components/cycles"; +import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { CycleService } from "services/cycle.service"; +// hooks // components -import { CycleForm } from "components/cycles"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; // constants -import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; type CycleModalProps = { isOpen: boolean; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index f01c840f1fc..06db83e0d60 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,32 +1,31 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; +import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; -import isEmpty from "lodash/isEmpty"; -// services -import { CycleService } from "services/cycle.service"; -// hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// icons +import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +// ui +import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; -// ui -import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// icons -import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +import { DateRangeDropdown } from "components/dropdowns"; +// constants +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// services +import { CycleService } from "services/cycle.service"; // types import { ICycle } from "@plane/types"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_UPDATED } from "constants/event-tracker"; -// fetch-keys -import { CYCLE_STATUS } from "constants/cycle"; -import { DateRangeDropdown } from "components/dropdowns"; type Props = { cycleId: string; @@ -299,7 +298,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { Date range
    -
    +
    void; diff --git a/web/components/dashboard/home-dashboard-widgets.tsx b/web/components/dashboard/home-dashboard-widgets.tsx index 2e2f9ef88b4..ab96ef90f61 100644 --- a/web/components/dashboard/home-dashboard-widgets.tsx +++ b/web/components/dashboard/home-dashboard-widgets.tsx @@ -1,7 +1,5 @@ import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useDashboard } from "hooks/store"; -// components import { AssignedIssuesWidget, CreatedIssuesWidget, @@ -13,6 +11,8 @@ import { RecentProjectsWidget, WidgetProps, } from "components/dashboard"; +import { useApplication, useDashboard } from "hooks/store"; +// components // types import { TWidgetKeys } from "@plane/types"; diff --git a/web/components/dashboard/project-empty-state.tsx b/web/components/dashboard/project-empty-state.tsx index bb7f82f341a..32236e233c1 100644 --- a/web/components/dashboard/project-empty-state.tsx +++ b/web/components/dashboard/project-empty-state.tsx @@ -1,13 +1,13 @@ -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Button } from "@plane/ui"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // assets import ProjectEmptyStateImage from "public/empty-state/dashboard/project.svg"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; export const DashboardProjectEmptyState = observer(() => { // store hooks diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 407ac9ddf24..3833d319c19 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -1,10 +1,8 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks -import { useDashboard } from "hooks/store"; -// components import { DurationFilterDropdown, TabsList, @@ -12,12 +10,14 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 23e7bee2771..61a1181e9cb 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -1,10 +1,8 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks -import { useDashboard } from "hooks/store"; -// components import { DurationFilterDropdown, TabsList, @@ -12,12 +10,14 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "created_issues"; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index fbdac4f00e0..3cf22c350e0 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -1,15 +1,14 @@ import { useState } from "react"; import { ChevronDown } from "lucide-react"; // components +import { CustomMenu } from "@plane/ui"; import { DateFilterModal } from "components/core"; // ui -import { CustomMenu } from "@plane/ui"; // helpers import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; // types import { EDurationFilters } from "@plane/types"; // constants -import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; type Props = { customDates?: string[]; diff --git a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx index f60d8efe60a..0cfad7dc91f 100644 --- a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/empty-states/created-issues.tsx b/web/components/dashboard/widgets/empty-states/created-issues.tsx index fe93d4404a6..2c59342fcaa 100644 --- a/web/components/dashboard/widgets/empty-states/created-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/created-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx index 716a3afc18d..a5279f715b3 100644 --- a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -1,11 +1,11 @@ -import { observer } from "mobx-react-lite"; import isToday from "date-fns/isToday"; +import { observer } from "mobx-react-lite"; // hooks -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 { useIssueDetail, useMember, useProject } from "hooks/store"; // types import { TIssue, TWidgetIssue } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index 16b2b95d9ee..c429f35999b 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; // hooks -import { useIssueDetail } from "hooks/store"; // components +import { Loader, getButtonStyling } from "@plane/ui"; import { AssignedCompletedIssueListItem, AssignedIssuesEmptyState, @@ -14,10 +14,10 @@ import { IssueListItemProps, } from "components/dashboard/widgets"; // ui -import { Loader, getButtonStyling } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper"; +import { useIssueDetail } from "hooks/store"; // types import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index d18f08f2755..d5fcea69711 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // helpers +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { cn } from "helpers/common.helper"; // types import { EDurationFilters, TIssuesListTypes } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { durationFilter: EDurationFilters; diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 3e9823fe4e9..a8a8f64e8c6 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,9 +1,8 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, @@ -12,11 +11,10 @@ import { WidgetProps, } from "components/dashboard/widgets"; // helpers -import { getCustomDates } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants import { IssuesByPriorityGraph } from "components/graphs"; +import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; const WIDGET_KEY = "issues_by_priority"; @@ -68,7 +66,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => })); return ( -
    +
    = observer((props) => />
    {totalCount > 0 ? ( -
    -
    +
    +
    { @@ -101,7 +99,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) =>
    ) : ( -
    +
    )} diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index b301d30f3fe..6ffeda0c48d 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -1,19 +1,21 @@ import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useDashboard } from "hooks/store"; -// components -import { PieGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByStateGroupEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { PieGraph } from "components/ui"; +import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; +import { STATE_GROUPS } from "constants/state"; import { getCustomDates } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { EDurationFilters, @@ -22,8 +24,6 @@ import { TStateGroups, } from "@plane/types"; // constants -import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; -import { STATE_GROUPS } from "constants/state"; const WIDGET_KEY = "issues_by_state_groups"; @@ -84,14 +84,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/components/dashboard/widgets/loaders/loader.tsx b/web/components/dashboard/widgets/loaders/loader.tsx index 141bb55336c..ae4038b38da 100644 --- a/web/components/dashboard/widgets/loaders/loader.tsx +++ b/web/components/dashboard/widgets/loaders/loader.tsx @@ -1,13 +1,13 @@ // components +import { TWidgetKeys } from "@plane/types"; import { AssignedIssuesWidgetLoader } from "./assigned-issues"; import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; import { OverviewStatsWidgetLoader } from "./overview-stats"; import { RecentActivityWidgetLoader } from "./recent-activity"; -import { RecentProjectsWidgetLoader } from "./recent-projects"; import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; +import { RecentProjectsWidgetLoader } from "./recent-projects"; // types -import { TWidgetKeys } from "@plane/types"; type Props = { widgetKey: TWidgetKeys; diff --git a/web/components/dashboard/widgets/overview-stats.tsx b/web/components/dashboard/widgets/overview-stats.tsx index 5a105cc1553..bfea5bf406b 100644 --- a/web/components/dashboard/widgets/overview-stats.tsx +++ b/web/components/dashboard/widgets/overview-stats.tsx @@ -2,14 +2,14 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; // hooks +import { WidgetLoader } from "components/dashboard/widgets"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { useDashboard } from "hooks/store"; // components -import { WidgetLoader } from "components/dashboard/widgets"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TOverviewStatsWidgetResponse } from "@plane/types"; -import { cn } from "helpers/common.helper"; export type WidgetProps = { dashboardId: string; @@ -74,6 +74,7 @@ export const OverviewStatsWidget: React.FC = observer((props) => { > {STATS_LIST.map((stat, index) => (
    = observer((props) => { if (!widgetStats) return ; return ( -
    - +
    + Your issue activities {widgetStats.length > 0 ? ( -
    +
    {widgetStats.map((activity) => (
    @@ -49,7 +49,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { activity.new_value === "restore" ? ( ) : ( -
    +
    ) @@ -89,14 +89,14 @@ export const RecentActivityWidget: React.FC = observer((props) => { href={redirectionLink} className={cn( getButtonStyling("link-primary", "sm"), - "w-min mx-auto py-1 px-2 text-xs hover:bg-custom-primary-100/20" + "mx-auto w-min px-2 py-1 text-xs hover:bg-custom-primary-100/20" )} > View all
    ) : ( -
    +
    )} diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx new file mode 100644 index 00000000000..438f87c4584 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators.tsx @@ -0,0 +1,94 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// hooks +import { Avatar } from "@plane/ui"; +import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +import { useDashboard, useMember, useUser } from "hooks/store"; +// components +// ui +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails) return null; + + return ( + +
    + +
    +
    + {isCurrentUser ? "You" : userDetails?.display_name} +
    +

    + {issueCount} active issue{issueCount > 1 ? "s" : ""} +

    + + ); +}); + +export const RecentCollaboratorsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { fetchWidgetStats, getWidgetStats } = useDashboard(); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + return ( +
    +
    +

    Most active members

    +

    + Top eight active members in your project by last activity +

    +
    + {widgetStats.length > 1 ? ( +
    + {widgetStats.map((user) => ( + + ))} +
    + ) : ( +
    + +
    + )} +
    + ); +}); diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx index 48c44807535..cfe7dd5caa9 100644 --- a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -1,15 +1,15 @@ import { useEffect } from "react"; -import Link from "next/link"; import { observer } from "mobx-react"; +import Link from "next/link"; import useSWR from "swr"; // store hooks +import { Avatar } from "@plane/ui"; import { useDashboard, useMember, useUser } from "hooks/store"; // components +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; import { WidgetLoader } from "../loaders"; // ui -import { Avatar } from "@plane/ui"; // types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; type CollaboratorListItemProps = { issueCount: number; diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx index d3f85782434..a27534bbf60 100644 --- a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; // components +import { Button } from "@plane/ui"; import { CollaboratorsList } from "./collaborators-list"; // ui -import { Button } from "@plane/ui"; type Props = { dashboardId: string; diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx index 5f611b46243..d65b15db7ae 100644 --- a/web/components/dashboard/widgets/recent-collaborators/root.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -1,11 +1,10 @@ import { useState } from "react"; import { Search } from "lucide-react"; +// types +import { WidgetProps } from "components/dashboard/widgets"; // components import { DefaultCollaboratorsList } from "./default-list"; import { SearchedCollaboratorsList } from "./search-list"; -8; -// types -import { WidgetProps } from "components/dashboard/widgets"; const PER_PAGE = 8; @@ -15,15 +14,15 @@ export const RecentCollaboratorsWidget: React.FC = (props) => { const [searchQuery, setSearchQuery] = useState(""); return ( -
    -
    +
    +

    Most active members

    Top eight active members in your project by last activity

    -
    +
    = (props) => { const ButtonToRender: React.FC = BORDER_BUTTON_VARIANTS.includes(variant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(variant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; return ( { +export const CycleOptions: FC = observer((props) => { const { projectId, isOpen, referenceElement, placement } = props; //state hooks diff --git a/web/components/dropdowns/cycle/index.tsx b/web/components/dropdowns/cycle/index.tsx index 2c05d9ddf06..8c08cd67de1 100644 --- a/web/components/dropdowns/cycle/index.tsx +++ b/web/components/dropdowns/cycle/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { ContrastIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useCycle } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { ContrastIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { CycleOptions } from "./cycle-options"; type Props = TDropdownProps & { diff --git a/web/components/dropdowns/date-range.tsx b/web/components/dropdowns/date-range.tsx index d3ef691b9b1..421ab41e643 100644 --- a/web/components/dropdowns/date-range.tsx +++ b/web/components/dropdowns/date-range.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { DateRange, DayPicker, Matcher } from "react-day-picker"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { ArrowRight, CalendarDays } from "lucide-react"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; // components -import { DropdownButton } from "./buttons"; // ui import { Button } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { DropdownButton } from "./buttons"; // types import { TButtonVariants } from "./types"; diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 570ea45da74..049bf225022 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -1,20 +1,20 @@ import React, { useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; import { DayPicker, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { CalendarDays, X } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { clearIconClassName?: string; diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 663ca67cec7..bc977d1ce61 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; +import sortBy from "lodash/sortBy"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search, Triangle } from "lucide-react"; -import sortBy from "lodash/sortBy"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication, useEstimate } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx index 067d609c5db..0f841b9e198 100644 --- a/web/components/dropdowns/member/avatar.tsx +++ b/web/components/dropdowns/member/avatar.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; import { useMember } from "hooks/store"; // ui -import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; type AvatarProps = { showTooltip: boolean; diff --git a/web/components/dropdowns/member/index.tsx b/web/components/dropdowns/member/index.tsx index 0513ec62713..0e9e36e2164 100644 --- a/web/components/dropdowns/member/index.tsx +++ b/web/components/dropdowns/member/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useMember } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { ButtonAvatars } from "./avatar"; import { DropdownButton } from "../buttons"; +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; +import { ButtonAvatars } from "./avatar"; // helpers -import { cn } from "helpers/common.helper"; // types +import { MemberOptions } from "./member-options"; import { MemberDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; -import { MemberOptions } from "./member-options"; type Props = { projectId?: string; diff --git a/web/components/dropdowns/member/member-options.tsx b/web/components/dropdowns/member/member-options.tsx index 46a0b9cbad4..d91c6e0b16e 100644 --- a/web/components/dropdowns/member/member-options.tsx +++ b/web/components/dropdowns/member/member-options.tsx @@ -1,16 +1,16 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { Avatar } from "@plane/ui"; //store import { useApplication, useMember, useUser } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; interface Props { projectId?: string; diff --git a/web/components/dropdowns/module/index.tsx b/web/components/dropdowns/module/index.tsx index 5e0a3977f55..88260471295 100644 --- a/web/components/dropdowns/module/index.tsx +++ b/web/components/dropdowns/module/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown, X } from "lucide-react"; // hooks +import { DiceIcon, Tooltip } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useModule } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { DiceIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { ModuleOptions } from "./module-options"; type Props = TDropdownProps & { @@ -71,7 +71,7 @@ const ButtonContent: React.FC = (props) => { {showCount ? (
    {!hideIcon && } -
    +
    {value.length > 0 ? value.length === 1 ? `${getModuleById(value[0])?.name || "module"}` @@ -80,18 +80,18 @@ const ButtonContent: React.FC = (props) => {
    ) : value.length > 0 ? ( -
    +
    {value.map((moduleId) => { const moduleDetails = getModuleById(moduleId); return (
    {!hideIcon && } {!hideText && ( - {moduleDetails?.name} + {moduleDetails?.name} )} {!disabled && ( @@ -266,8 +266,7 @@ export const ModuleDropdown: React.FC = observer((props) => { placeholder={placeholder} showCount={showCount} value={value} - // @ts-ignore - onChange={onChange} + onChange={onChange as any} /> diff --git a/web/components/dropdowns/module/module-options.tsx b/web/components/dropdowns/module/module-options.tsx index e7d205b126d..8f6a664688f 100644 --- a/web/components/dropdowns/module/module-options.tsx +++ b/web/components/dropdowns/module/module-options.tsx @@ -1,17 +1,17 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { DiceIcon } from "@plane/ui"; //store +import { cn } from "helpers/common.helper"; import { useApplication, useModule } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; -import { cn } from "helpers/common.helper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; type DropdownOptions = | { diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index e0677c84371..2409971f34d 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { useTheme } from "next-themes"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; -import { useTheme } from "next-themes"; // hooks +import { PriorityIcon, Tooltip } from "@plane/ui"; +import { ISSUE_PRIORITIES } from "constants/issue"; +import { cn } from "helpers/common.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons -import { PriorityIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssuePriorities } from "@plane/types"; +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { ISSUE_PRIORITIES } from "constants/issue"; -import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -342,8 +342,8 @@ export const PriorityDropdown: React.FC = (props) => { const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; useEffect(() => { if (isOpen && inputRef.current) { diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index f6fb9205e86..05b455e5e89 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -81,8 +81,8 @@ export const ProjectDropdown: React.FC = observer((props) => { {projectDetails?.emoji ? renderEmoji(projectDetails?.emoji) : projectDetails?.icon_prop - ? renderEmoji(projectDetails?.icon_prop) - : null} + ? renderEmoji(projectDetails?.icon_prop) + : null} {projectDetails?.name}
    @@ -174,8 +174,8 @@ export const ProjectDropdown: React.FC = observer((props) => { {selectedProject?.emoji ? renderEmoji(selectedProject?.emoji) : selectedProject?.icon_prop - ? renderEmoji(selectedProject?.icon_prop) - : null} + ? renderEmoji(selectedProject?.icon_prop) + : null} )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index 9fa2f38c84e..f34ef576cc3 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -1,22 +1,22 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useApplication, useProjectState } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // icons -import { StateGroupIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 9c45e535687..b9211a718e2 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useState, useRef } from "react"; // headless ui +import { TwitterPicker } from "react-color"; import { Tab, Transition, Popover } from "@headlessui/react"; // react colors -import { TwitterPicker } from "react-color"; // hooks +import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types -import { Props } from "./types"; // emojis import emojis from "./emojis.json"; +import { getRecentEmojis, saveRecentEmoji } from "./helpers"; import icons from "./icons.json"; // helpers -import { getRecentEmojis, saveRecentEmoji } from "./helpers"; -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +import { Props } from "./types"; const tabOptions = [ { diff --git a/web/components/empty-state/comic-box-button.tsx b/web/components/empty-state/comic-box-button.tsx index 607d74a9171..0bf546a2f08 100644 --- a/web/components/empty-state/comic-box-button.tsx +++ b/web/components/empty-state/comic-box-button.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; +import { usePopper } from "react-popper"; import { Popover } from "@headlessui/react"; // popper -import { usePopper } from "react-popper"; // helper import { getButtonStyling } from "@plane/ui"; diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 4a5aeca0291..9d77a81d0cd 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,11 +1,11 @@ import React from "react"; import Image from "next/image"; // components -import { ComicBoxButton } from "./comic-box-button"; // ui import { Button, getButtonStyling } from "@plane/ui"; // helper import { cn } from "helpers/common.helper"; +import { ComicBoxButton } from "./comic-box-button"; type Props = { title: string; diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 1ca39c84abf..3be83e31904 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -1,14 +1,14 @@ import React, { useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // store hooks +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { checkDuplicates } from "helpers/array.helper"; import { useEstimate } from "hooks/store"; // ui -import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { checkDuplicates } from "helpers/array.helper"; // types import { IEstimate, IEstimateFormData } from "@plane/types"; @@ -269,7 +269,7 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { {Array(6) .fill(0) .map((_, i) => ( -
    +
    {i + 1} diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index ac51d231273..f8bc2a65b7b 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,14 +1,14 @@ import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useEstimate } from "hooks/store"; // types import { IEstimate } from "@plane/types"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; @@ -29,6 +29,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const handleEstimateDelete = () => { if (!workspaceSlug || !projectId) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain const estimateId = data?.id!; deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index 37932a0accf..c63c4b2085a 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Pencil, Trash2 } from "lucide-react"; +import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { orderArrayBy } from "helpers/array.helper"; import { useProject } from "hooks/store"; // ui -import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; //icons -import { Pencil, Trash2 } from "lucide-react"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IEstimate } from "@plane/types"; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 711f713a670..8e447d6ac5b 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,20 +1,20 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // store hooks +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { orderArrayBy } from "helpers/array.helper"; import { useEstimate, useProject, useUser } from "hooks/store"; // components -import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states diff --git a/web/components/exporter/export-modal.tsx b/web/components/exporter/export-modal.tsx index f38550b3a28..16f8d464017 100644 --- a/web/components/exporter/export-modal.tsx +++ b/web/components/exporter/export-modal.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // hooks +import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; + import { useProject } from "hooks/store"; // services import { ProjectExportService } from "services/project"; // ui -import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser, IImporterService } from "@plane/types"; diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index ed6a392207d..381b168bd45 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -1,30 +1,29 @@ import { useState } from "react"; -import Link from "next/link"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; +import { Button } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { Exporter, SingleExport } from "components/exporter"; +import { ImportExportSettingsLoader } from "components/ui"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +import { EXPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components -import { Exporter, SingleExport } from "components/exporter"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { ImportExportSettingsLoader } from "components/ui"; // icons -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // fetch-keys -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; // constants -import { EXPORTERS_LIST } from "constants/workspace"; - -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; // services const integrationService = new IntegrationService(); diff --git a/web/components/exporter/single-export.tsx b/web/components/exporter/single-export.tsx index 34e41fc3585..4fdcb4a15e3 100644 --- a/web/components/exporter/single-export.tsx +++ b/web/components/exporter/single-export.tsx @@ -38,12 +38,12 @@ export const SingleExport: FC = ({ service, refreshing }) => { service.status === "completed" ? "bg-green-500/20 text-green-500" : service.status === "processing" - ? "bg-yellow-500/20 text-yellow-500" - : service.status === "failed" - ? "bg-red-500/20 text-red-500" - : service.status === "expired" - ? "bg-orange-500/20 text-orange-500" - : "" + ? "bg-yellow-500/20 text-yellow-500" + : service.status === "failed" + ? "bg-red-500/20 text-red-500" + : service.status === "expired" + ? "bg-orange-500/20 text-orange-500" + : "" }`} > {refreshing ? "Refreshing..." : service.status} diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx index 1e0882aeec2..3305c9846e0 100644 --- a/web/components/gantt-chart/blocks/block.tsx +++ b/web/components/gantt-chart/blocks/block.tsx @@ -1,16 +1,16 @@ import { observer } from "mobx-react"; // hooks -import { useGanttChart } from "../hooks"; -import { useIssueDetail } from "hooks/store"; // components -import { ChartAddBlock, ChartDraggable } from "../helpers"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants import { BLOCK_HEIGHT } from "../constants"; +import { ChartAddBlock, ChartDraggable } from "../helpers"; +import { useGanttChart } from "../hooks"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index d98524ecc40..8eb1d877252 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; // components +import { HEADER_HEIGHT } from "../constants"; +import { IBlockUpdateData, IGanttBlock } from "../types"; import { GanttChartBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants -import { HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -47,6 +47,7 @@ export const GanttChartBlocksList: FC = (props) => { return ( = observer(() => { // chart hook diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 1d8a19f1a60..752645f6682 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -1,4 +1,4 @@ -import { createContext } from "react"; +import React, { FC, createContext } from "react"; // mobx store import { GanttStore } from "store/issue/issue_gantt_view.store"; @@ -7,13 +7,17 @@ let ganttViewStore = new GanttStore(); export const GanttStoreContext = createContext(ganttViewStore); const initializeStore = () => { - const _ganttStore = ganttViewStore ?? new GanttStore(); - if (typeof window === "undefined") return _ganttStore; - if (!ganttViewStore) ganttViewStore = _ganttStore; - return _ganttStore; + const newGanttViewStore = ganttViewStore ?? new GanttStore(); + if (typeof window === "undefined") return newGanttViewStore; + if (!ganttViewStore) ganttViewStore = newGanttViewStore; + return newGanttViewStore; }; -export const GanttStoreProvider = ({ children }: any) => { +type GanttStoreProviderProps = { + children: React.ReactNode; +}; + +export const GanttStoreProvider: FC = ({ children }) => { const store = initializeStore(); return {children}; }; diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx index b7497013fb5..d12c9f20e2b 100644 --- a/web/components/gantt-chart/helpers/add-block.tsx +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { addDays } from "date-fns"; +import { observer } from "mobx-react"; import { Plus } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index c2b4dc61914..54590c37256 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; import { ArrowRight } from "lucide-react"; // hooks import { IGanttBlock } from "components/gantt-chart"; @@ -7,7 +8,6 @@ import { cn } from "helpers/common.helper"; // constants import { SIDEBAR_WIDTH } from "../constants"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx index f1374c7531d..6e780c4799a 100644 --- a/web/components/gantt-chart/sidebar/cycles/block.tsx +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { CycleGanttSidebarBlock } from "components/cycles"; +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components -import { CycleGanttSidebarBlock } from "components/cycles"; // helpers +import { IGanttBlock } from "components/gantt-chart/types"; import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx index 11f67a099f3..e47b2304ef9 100644 --- a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { CyclesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/issues/block.tsx b/web/components/gantt-chart/sidebar/issues/block.tsx index 03a17a65b03..92fc32664a5 100644 --- a/web/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/components/gantt-chart/sidebar/issues/block.tsx @@ -2,17 +2,17 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks -import { useIssueDetail } from "hooks/store"; import { useGanttChart } from "components/gantt-chart/hooks"; // components import { IssueGanttSidebarBlock } from "components/issues"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IGanttBlock } from "../../types"; // constants import { BLOCK_HEIGHT } from "../../constants"; +import { IGanttBlock } from "../../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index 323938eec95..e82e40f5dd7 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -1,10 +1,10 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; // components -import { IssuesSidebarBlock } from "./block"; // ui import { Loader } from "@plane/ui"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; +import { IssuesSidebarBlock } from "./block"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx index 4b2e4722638..41647644f70 100644 --- a/web/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components +import { IGanttBlock } from "components/gantt-chart/types"; import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx index dee83fa79af..a4bcbd5ec16 100644 --- a/web/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ModulesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/project-views.tsx b/web/components/gantt-chart/sidebar/project-views.tsx index a7e7c5e35ad..92a677b1927 100644 --- a/web/components/gantt-chart/sidebar/project-views.tsx +++ b/web/components/gantt-chart/sidebar/project-views.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { IssuesSidebarBlock } from "./issues/block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/views/bi-week-view.ts b/web/components/gantt-chart/views/bi-week-view.ts index 14c0aad15e4..6ace4bcc48e 100644 --- a/web/components/gantt-chart/views/bi-week-view.ts +++ b/web/components/gantt-chart/views/bi-week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/day-view.ts b/web/components/gantt-chart/views/day-view.ts index 0801b7bb187..e8da6801cc5 100644 --- a/web/components/gantt-chart/views/day-view.ts +++ b/web/components/gantt-chart/views/day-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/helpers.ts b/web/components/gantt-chart/views/helpers.ts index 94b614286ac..4bd295ce357 100644 --- a/web/components/gantt-chart/views/helpers.ts +++ b/web/components/gantt-chart/views/helpers.ts @@ -56,8 +56,8 @@ export const getAllDatesInWeekByWeekNumber = (weekNumber: number, year: number) const startDate = new Date(firstDayOfYear.getTime()); startDate.setDate(startDate.getDate() + 7 * (weekNumber - 1)); - var datesInWeek = []; - for (var i = 0; i < 7; i++) { + const datesInWeek = []; + for (let i = 0; i < 7; i++) { const currentDate = new Date(startDate.getTime()); currentDate.setDate(currentDate.getDate() + i); datesInWeek.push(currentDate); diff --git a/web/components/gantt-chart/views/hours-view.ts b/web/components/gantt-chart/views/hours-view.ts index 0801b7bb187..e8da6801cc5 100644 --- a/web/components/gantt-chart/views/hours-view.ts +++ b/web/components/gantt-chart/views/hours-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index 13d054da1ab..1e7e6d87816 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType, IGanttBlock } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; @@ -178,7 +178,7 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24))); scrollPosition = positionDaysDifference * chartData.data.width; - var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; + let diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; diffMonths -= startDate.getMonth(); diffMonths += itemStartDate.getMonth(); diff --git a/web/components/gantt-chart/views/quater-view.ts b/web/components/gantt-chart/views/quater-view.ts index ed25974a3a5..9d45a43a131 100644 --- a/web/components/gantt-chart/views/quater-view.ts +++ b/web/components/gantt-chart/views/quater-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/gantt-chart/views/week-view.ts b/web/components/gantt-chart/views/week-view.ts index a65eb70b95c..bd4ae383d61 100644 --- a/web/components/gantt-chart/views/week-view.ts +++ b/web/components/gantt-chart/views/week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/year-view.ts b/web/components/gantt-chart/views/year-view.ts index 82d397e97c2..69ff9dae890 100644 --- a/web/components/gantt-chart/views/year-view.ts +++ b/web/components/gantt-chart/views/year-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/graphs/issues-by-priority.tsx b/web/components/graphs/issues-by-priority.tsx index 0d4bf37b5fe..9dfe568917a 100644 --- a/web/components/graphs/issues-by-priority.tsx +++ b/web/components/graphs/issues-by-priority.tsx @@ -1,14 +1,14 @@ -import { Theme } from "@nivo/core"; import { ComputedDatum } from "@nivo/bar"; +import { Theme } from "@nivo/core"; // components import { BarGraph } from "components/ui"; // helpers +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { TIssuePriorities } from "@plane/types"; // constants -import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; -import { ISSUE_PRIORITIES } from "constants/issue"; type Props = { borderRadius?: number; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 7cdc2313319..18d0543c01d 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,8 +1,20 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, Plus, PanelRight } from "lucide-react"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -16,24 +28,12 @@ import { } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // icons -import { ArrowRight, Plus, PanelRight } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { // router @@ -209,9 +209,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => ( - - ))} + {currentProjectCycleIds?.map((cycleId) => )} } /> diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 496fabecd46..a0ab19ec72c 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -1,20 +1,20 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { List, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { TCycleLayout } from "@plane/types"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { TCycleLayout } from "@plane/types"; export const CyclesHeader: FC = observer(() => { // router @@ -73,7 +73,9 @@ export const CyclesHeader: FC = observer(() => { /> } />} + link={ + } /> + } />
    @@ -110,6 +112,7 @@ export const CyclesHeader: FC = observer(() => { > {CYCLE_VIEW_LAYOUTS.map((layout) => ( { // handleLayoutChange(ISSUE_LAYOUTS[index].key); handleCurrentLayout(layout.key as TCycleLayout); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 3c40cbbffd1..effe60fe42e 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -1,23 +1,23 @@ import { useCallback, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useLabel, useMember, useUser, useIssues } from "hooks/store"; -// components +import { List, PlusIcon, Sheet } from "lucide-react"; +import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; +// components import { CreateUpdateWorkspaceViewModal } from "components/workspace"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; // icons -import { List, PlusIcon, Sheet } from "lucide-react"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserWorkspaceRoles } from "constants/workspace"; +import { useLabel, useMember, useUser, useIssues } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; const GLOBAL_VIEW_LAYOUTS = [ { key: "list", title: "List", link: "/workspace-views", icon: List }, diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index b84504ee21b..ca3a84e3b75 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -1,8 +1,20 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, PanelRight, Plus } from "lucide-react"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { ModuleMobileHeader } from "components/modules/module-mobile-header"; +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -16,24 +28,12 @@ import { } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, CustomMenu, DiceIcon, LayersIcon } from "@plane/ui"; // icons -import { ArrowRight, PanelRight, Plus } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { ModuleMobileHeader } from "components/modules/module-mobile-header"; const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { // router @@ -212,9 +212,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => ( - - ))} + {projectModuleIds?.map((moduleId) => )} } /> diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 9ad34678a72..b942b7b136f 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,19 +1,20 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +// icons import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // ui import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui"; -// helper -import { renderEmoji } from "helpers/emoji.helper"; +// components +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +// helper +import { renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -71,14 +72,16 @@ export const ModulesListHeader: React.FC = observer(() => { @@ -106,7 +109,13 @@ export const ModulesListHeader: React.FC = observer(() => { // placement="bottom-start" customButton={ - {modulesView === 'gantt_chart' ? : modulesView === 'grid' ? : } + {modulesView === "gantt_chart" ? ( + + ) : modulesView === "grid" ? ( + + ) : ( + + )} Layout } @@ -115,6 +124,7 @@ export const ModulesListHeader: React.FC = observer(() => { > {MODULE_VIEW_LAYOUTS.map((layout) => ( setModulesView(layout.key)} className="flex items-center gap-2" > @@ -127,5 +137,3 @@ export const ModulesListHeader: React.FC = observer(() => {
    ); }); - - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index e2a427db78c..0eed7217875 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -1,16 +1,16 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, usePage, useProject } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { renderEmoji } from "helpers/emoji.helper"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, usePage, useProject } from "hooks/store"; export interface IPagesHeaderProps { showButton?: boolean; diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 1984971d6ed..b5ce74fc5b8 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // constants -import { EUserProjectRoles } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const PagesHeader = observer(() => { // router diff --git a/web/components/headers/profile-settings.tsx b/web/components/headers/profile-settings.tsx index 24c69f0935d..5c419f05b9d 100644 --- a/web/components/headers/profile-settings.tsx +++ b/web/components/headers/profile-settings.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; // ui -import { Breadcrumbs } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; interface IProfileSettingHeader { diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 9d4596f8357..8752e739630 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -1,22 +1,22 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks +import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; // types +import { IssueArchiveService } from "services/issue"; import { TIssue } from "@plane/types"; // constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; // services -import { IssueArchiveService } from "services/issue"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; const issueArchiveService = new IssueArchiveService(); diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index d1da1c85971..8ade61aae36 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -1,19 +1,19 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ArrowLeft } from "lucide-react"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; // components -import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helpers import { renderEmoji } from "helpers/emoji.helper"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 139ec025797..3fd0cb39939 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -1,17 +1,17 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; // helper -import { renderEmoji } from "helpers/emoji.helper"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { renderEmoji } from "helpers/emoji.helper"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ProjectDraftIssueHeader: FC = observer(() => { diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b5260edd7ec..b89fbaaacb1 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -1,17 +1,17 @@ import { FC, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useProject } from "hooks/store"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components -import { CreateInboxIssueModal } from "components/inbox"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CreateInboxIssueModal } from "components/inbox"; // helper import { renderEmoji } from "helpers/emoji.helper"; +import { useProject } from "hooks/store"; export const ProjectInboxHeader: FC = observer(() => { // states diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 3732f2598af..2f6349e613c 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -1,22 +1,22 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks +import { PanelRight } from "lucide-react"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useProject } from "hooks/store"; // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // services import { IssueService } from "services/issue"; // constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -import { PanelRight } from "lucide-react"; -import { cn } from "helpers/common.helper"; // services const issueService = new IssueService(); @@ -91,7 +91,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
    ); diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 43030c5c2d0..8e8807fdbf6 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,8 +1,17 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks +import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, @@ -13,21 +22,12 @@ import { useMember, } from "hooks/store"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // types +import { useIssues } from "hooks/store/use-issues"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helper -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues } from "hooks/store/use-issues"; -import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; export const ProjectIssuesHeader: React.FC = observer(() => { // states diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index b70a4614f55..87b2e507e0d 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -1,17 +1,17 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // ui import { Breadcrumbs, CustomMenu } from "@plane/ui"; // helper +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useProject, useUser } from "hooks/store"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; export interface IProjectSettingHeader { title: string; diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 175534a79e7..eea21143164 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -1,9 +1,22 @@ import { useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { Plus } from "lucide-react"; // hooks +// components +// ui +import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +// helpers +// types +// constants +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { renderEmoji } from "helpers/emoji.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -15,20 +28,7 @@ import { useProjectView, useUser, } from "hooks/store"; -// components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui -import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; -// helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; -// constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index bb070a22f57..3b4d7fb203e 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useApplication, useProject, useUser } from "hooks/store"; // components import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // helpers +import { EUserProjectRoles } from "constants/project"; import { renderEmoji } from "helpers/emoji.helper"; // constants -import { EUserProjectRoles } from "constants/project"; +import { useApplication, useProject, useUser } from "hooks/store"; export const ProjectViewsHeader: React.FC = observer(() => { // router diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index f6dd7fd3cae..3810860aa6f 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; import { Search, Plus, Briefcase } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // constants +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserWorkspaceRoles } from "constants/workspace"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const ProjectsHeader = observer(() => { // store hooks diff --git a/web/components/headers/user-profile.tsx b/web/components/headers/user-profile.tsx index 30bc5b2a961..09b764cdca9 100644 --- a/web/components/headers/user-profile.tsx +++ b/web/components/headers/user-profile.tsx @@ -1,23 +1,23 @@ // ui +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { ChevronDown, PanelRight } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; // components import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile"; import { cn } from "helpers/common.helper"; -import { FC } from "react"; import { useApplication, useUser } from "hooks/store"; -import { ChevronDown, PanelRight } from "lucide-react"; -import { observer } from "mobx-react-lite"; -import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile"; -import Link from "next/link"; -import { useRouter } from "next/router"; type TUserProfileHeader = { - type?: string | undefined -} + type?: string | undefined; +}; export const UserProfileHeader: FC = observer((props) => { - const { type = undefined } = props + const { type = undefined } = props; const router = useRouter(); const { workspaceSlug, userId } = router.query; @@ -34,45 +34,60 @@ export const UserProfileHeader: FC = observer((props) => { const { theme: themStore } = useApplication(); - return (
    -
    - -
    - - } /> - -
    - - {type} - -
    - } - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - <> - {tabsList.map((tab) => ( - - {tab.label} - - ))} - - + return ( +
    +
    + +
    + + } + /> + +
    + + {type} + +
    + } + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + <> + {tabsList.map((tab) => ( + + + {tab.label} + + + ))} + + +
    -
    ) + ); }); - - diff --git a/web/components/headers/workspace-active-cycles.tsx b/web/components/headers/workspace-active-cycles.tsx index 195b8947159..a33161de948 100644 --- a/web/components/headers/workspace-active-cycles.tsx +++ b/web/components/headers/workspace-active-cycles.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; // ui +import { Crown } from "lucide-react"; import { Breadcrumbs, ContrastIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // icons -import { Crown } from "lucide-react"; export const WorkspaceActiveCycleHeader = observer(() => (
    diff --git a/web/components/headers/workspace-analytics.tsx b/web/components/headers/workspace-analytics.tsx index a6ad67f05e0..2bede32bad7 100644 --- a/web/components/headers/workspace-analytics.tsx +++ b/web/components/headers/workspace-analytics.tsx @@ -1,14 +1,14 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { BarChart2, PanelRight } from "lucide-react"; // ui import { Breadcrumbs } from "@plane/ui"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { cn } from "helpers/common.helper"; -import { useEffect } from "react"; +import { useApplication } from "hooks/store"; export const WorkspaceAnalyticsHeader = observer(() => { const router = useRouter(); @@ -47,11 +47,21 @@ export const WorkspaceAnalyticsHeader = observer(() => { } /> - {analytics_tab === 'custom' && - - } + )}
    diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index 6b85577f67d..e7ae3c726e6 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -1,17 +1,17 @@ -import { LayoutGrid, Zap } from "lucide-react"; import Image from "next/image"; import { useTheme } from "next-themes"; +import { LayoutGrid, Zap } from "lucide-react"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; // hooks -import { useEventTracker } from "hooks/store"; // components -import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker"; +import { useEventTracker } from "hooks/store"; export const WorkspaceDashboardHeader = () => { // hooks diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx index 5ced552048e..faf1a45d145 100644 --- a/web/components/headers/workspace-settings.tsx +++ b/web/components/headers/workspace-settings.tsx @@ -1,14 +1,14 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // ui -import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs, CustomMenu } from "@plane/ui"; // hooks -import { observer } from "mobx-react-lite"; // components +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; -import { BreadcrumbLink } from "components/common"; export interface IWorkspaceSettingHeader { title: string; diff --git a/web/components/icons/priority-icon.tsx b/web/components/icons/priority-icon.tsx index b23f56eabac..36ea67b3de0 100644 --- a/web/components/icons/priority-icon.tsx +++ b/web/components/icons/priority-icon.tsx @@ -14,12 +14,12 @@ export const PriorityIcon: React.FC = ({ priority, className = "" }) => { {priority === "urgent" ? "error" : priority === "high" - ? "signal_cellular_alt" - : priority === "medium" - ? "signal_cellular_alt_2_bar" - : priority === "low" - ? "signal_cellular_alt_1_bar" - : "block"} + ? "signal_cellular_alt" + : priority === "medium" + ? "signal_cellular_alt_2_bar" + : priority === "low" + ? "signal_cellular_alt_1_bar" + : "block"} ); }; diff --git a/web/components/icons/state/state-group-icon.tsx b/web/components/icons/state/state-group-icon.tsx index 15debf5f22f..ae9e5f1a94f 100644 --- a/web/components/icons/state/state-group-icon.tsx +++ b/web/components/icons/state/state-group-icon.tsx @@ -7,9 +7,9 @@ import { StateGroupUnstartedIcon, } from "components/icons"; // types +import { STATE_GROUPS } from "constants/state"; import { TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { className?: string; diff --git a/web/components/inbox/content/root.tsx b/web/components/inbox/content/root.tsx index 26f58131e77..7cc19bec3f3 100644 --- a/web/components/inbox/content/root.tsx +++ b/web/components/inbox/content/root.tsx @@ -2,12 +2,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Inbox } from "lucide-react"; // hooks -import { useInboxIssues } from "hooks/store"; -// components +import { Loader } from "@plane/ui"; import { InboxIssueActionsHeader } from "components/inbox"; import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox"; +import { useInboxIssues } from "hooks/store"; +// components // ui -import { Loader } from "@plane/ui"; type TInboxContentRoot = { workspaceSlug: string; diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 4d4cfa0cca6..661bc2d7244 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -1,10 +1,12 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { DayPicker } from "react-day-picker"; import { Popover } from "@headlessui/react"; -// hooks -import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; +// icons +import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { AcceptIssueModal, @@ -12,14 +14,12 @@ import { DeleteInboxIssueModal, SelectDuplicateInboxIssueModal, } from "components/inbox"; -// ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; +import { ISSUE_DELETED } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +// hooks +import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; // types import type { TInboxDetailedStatus } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; -import { ISSUE_DELETED } from "constants/event-tracker"; type TInboxIssueActionsHeader = { workspaceSlug: string; @@ -232,7 +232,7 @@ export const InboxIssueActionsHeader: FC = observer((p )} {inboxIssueId && ( -
    +
    diff --git a/web/components/issues/attachment/attachment-detail.tsx b/web/components/issues/attachment/attachment-detail.tsx index 0d345a6191f..8ff2b9305ce 100644 --- a/web/components/issues/attachment/attachment-detail.tsx +++ b/web/components/issues/attachment/attachment-detail.tsx @@ -2,17 +2,17 @@ import { FC, useState } from "react"; import Link from "next/link"; import { AlertCircle, X } from "lucide-react"; // hooks -import { useIssueDetail, useMember } from "hooks/store"; // ui import { Tooltip } from "@plane/ui"; // components -import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // icons import { getFileIcon } from "components/icons"; // helper -import { truncateText } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { truncateText } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index bf197980aa1..27dc572a90b 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -2,11 +2,11 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; // hooks -import { useApplication } from "hooks/store"; // constants import { MAX_FILE_SIZE } from "constants/common"; // helpers import { generateFileName } from "helpers/attachment.helper"; +import { useApplication } from "hooks/store"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 2129a4f61b6..0f834c1a49e 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -32,6 +32,7 @@ export const IssueAttachmentsList: FC = observer((props) issueAttachments.length > 0 && issueAttachments.map((attachmentId) => ( = observer((props) => { debouncedFormSave(); }} required - className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" + className="block min-h-min w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" hasError={Boolean(errors?.name)} role="textbox" /> @@ -173,7 +173,7 @@ export const IssueDescriptionForm: FC = observer((props) => { setIsSubmitting={setIsSubmitting} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 65e82df5fc3..4f1f5c05671 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -1,16 +1,15 @@ import { FC, useState, useEffect } from "react"; // components -import { Loader } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; -// store hooks +import { Loader } from "@plane/ui"; +// hooks import { useMention, useWorkspace } from "hooks/store"; +import useDebounce from "hooks/use-debounce"; // services import { FileService } from "services/file.service"; const fileService = new FileService(); // types import { TIssueOperations } from "./issue-detail"; -// hooks -import useDebounce from "hooks/use-debounce"; export type IssueDescriptionInputProps = { workspaceSlug: string; @@ -78,7 +77,7 @@ export const IssueDescriptionInput: FC = (props) => initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setIsSubmitting("submitting"); setDescriptionHTML(description_html === "" ? "

    " : description_html); }} diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index 4da762a9dbc..8744857c1e7 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { CycleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -41,14 +40,14 @@ export const IssueCycleSelect: React.FC = observer((props) => }; return ( -
    +
    = observer((props) => { projectId={projectId} inboxId={inboxId} issueId={issueId} - showDescription={true} + showDescription />
    diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index 9b0e961c0bb..144198085dc 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -2,17 +2,16 @@ import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // components -import { InboxIssueMainContent } from "./main-content"; -import { InboxIssueDetailsSidebar } from "./sidebar"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useInboxIssues, useIssueDetail, useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; +import { InboxIssueMainContent } from "./main-content"; +import { InboxIssueDetailsSidebar } from "./sidebar"; // constants -import { EUserProjectRoles } from "constants/project"; export type TInboxIssueDetailRoot = { workspaceSlug: string; @@ -48,12 +47,7 @@ export const InboxIssueDetailRoot: FC = (props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - ) => { + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); captureIssueEvent({ diff --git a/web/components/issues/issue-detail/inbox/sidebar.tsx b/web/components/issues/issue-detail/inbox/sidebar.tsx index 592791a85b1..bf9e833ceee 100644 --- a/web/components/issues/issue-detail/inbox/sidebar.tsx +++ b/web/components/issues/issue-detail/inbox/sidebar.tsx @@ -2,14 +2,14 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { CalendarCheck2, Signal, Tag } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // components -import { IssueLabel, TIssueOperations } from "components/issues"; +import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; +import { IssueLabel, TIssueOperations } from "components/issues"; // icons -import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; // helper import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 575e8d8414a..af32660679a 100644 --- a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -31,6 +31,7 @@ export const IssueActivityCommentRoot: FC = observer( {activityComments.map((activityComment, index) => activityComment.activity_type === "COMMENT" ? ( = observer((props > <> {activity.old_value === "" ? `added a new assignee ` : `removed the assignee `} - = observer((props > {activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value} - {showIssue && (activity.old_value === "" ? ` to ` : ` from `)} {showIssue && }. diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx index 8336e516f22..ec3c777fc64 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { ContrastIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { ContrastIcon } from "@plane/ui"; type TIssueCycleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx index e4538753569..0eeb7ecacc3 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { LayersIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { LayersIcon } from "@plane/ui"; type TIssueDefaultActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx index e01b94e1b34..a8c309bd584 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx @@ -33,13 +33,11 @@ export const IssueEstimateActivity: FC = observer((props {activity.new_value ? `set the estimate point to ` : `removed the estimate point `} {activity.new_value && ( <> - {areEstimatesEnabledForCurrentProject ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} - )} {showIssue && (activity.new_value ? ` to ` : ` from `)} diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index e209b4bbf23..0097b65b6fb 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { Network } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; // components import { IssueUser } from "../"; // helpers -import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; type TIssueActivityBlockComponent = { icon?: ReactNode; @@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC = (pr ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2` }`} > -
    +
    {icon ? icon : }
    diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index e86b1fb57e3..49f813ec640 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // hooks +import { Tooltip } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; type TIssueLink = { activityId: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx index c8089d23308..0108c56b334 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DiceIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { DiceIcon } from "@plane/ui"; type TIssueModuleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx index e68a7c37371..5ef67cf52f3 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx @@ -1,13 +1,13 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { issueRelationObject } from "components/issues/issue-detail/relation-select"; import { useIssueDetail } from "hooks/store"; // components +import { TIssueRelationTypes } from "@plane/types"; import { IssueActivityBlockComponent } from "./"; // component helpers -import { issueRelationObject } from "components/issues/issue-detail/relation-select"; // types -import { TIssueRelationTypes } from "@plane/types"; type TIssueRelationActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx index 95b3cda80ec..0e3a80b3475 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueStartDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx index 7cc47c2c8ae..75751938873 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DoubleCircleIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // icons -import { DoubleCircleIcon } from "@plane/ui"; type TIssueStateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx index a4b40ec3106..947b2e6e6dc 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueTargetDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/root.tsx b/web/components/issues/issue-detail/issue-activity/activity/root.tsx index af44463d5a6..092633b06e2 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/root.tsx @@ -23,6 +23,7 @@ export const IssueActivityRoot: FC = observer((props) => {
    {activityIds.map((activityId, index) => ( diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx index 4dbc36f6ba0..b00dd2a1305 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; import { MessageCircle } from "lucide-react"; // hooks +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; type TIssueCommentBlock = { commentId: string; @@ -24,7 +24,7 @@ export const IssueCommentBlock: FC = (props) => { if (!comment) return <>; return (
    -
    +
    {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( = (props) => { value={watch("comment_html") ?? ""} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)} + onChange={(comment_json: any, comment_html: string) => setValue("comment_html", comment_html)} mentionSuggestions={mentionSuggestions} mentionHighlights={mentionHighlights} /> @@ -150,7 +150,7 @@ export const IssueCommentCard: FC = (props) => { onClick={handleSubmit(onEnter)} disabled={isSubmitting || isEmpty} className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${ - isEmpty ? "bg-gray-200 cursor-not-allowed" : "hover:bg-green-500" + isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500" }`} > = (props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: any, comment_html: string) => { onChange(comment_html); }} mentionSuggestions={mentionSuggestions} diff --git a/web/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/components/issues/issue-detail/issue-activity/comments/root.tsx index 4e2775c4ae5..0696fa12946 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TActivityOperations } from "../root"; import { IssueCommentCard } from "./comment-card"; // types -import { TActivityOperations } from "../root"; type TIssueCommentRoot = { workspaceSlug: string; @@ -28,6 +28,7 @@ export const IssueCommentRoot: FC = observer((props) => {
    {commentIds.map((commentId, index) => ( = (props) => { return ( <>
    @@ -149,7 +149,7 @@ export const LabelCreate: FC = (props) => { )} diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx index 60792b01c62..69c0e08e95b 100644 --- a/web/components/issues/issue-detail/label/label-list-item.tsx +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import { X } from "lucide-react"; // types -import { TLabelOperations } from "./root"; import { useIssueDetail, useLabel } from "hooks/store"; +import { TLabelOperations } from "./root"; type TLabelListItem = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/label/label-list.tsx b/web/components/issues/issue-detail/label/label-list.tsx index fd714e002b6..fdf94be28bf 100644 --- a/web/components/issues/issue-detail/label/label-list.tsx +++ b/web/components/issues/issue-detail/label/label-list.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { useIssueDetail } from "hooks/store"; import { LabelListItem } from "./label-list-item"; // hooks -import { useIssueDetail } from "hooks/store"; // types import { TLabelOperations } from "./root"; @@ -29,6 +29,7 @@ export const LabelList: FC = (props) => { <> {issueLabels.map((labelId) => ( = observer((props) => // states const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [query, setQuery] = useState(""); const issue = getIssueById(issueId); @@ -71,7 +71,7 @@ export const IssueLabelSelect: React.FC = observer((props) => const label = (
    @@ -102,7 +102,7 @@ export const IssueLabelSelect: React.FC = observer((props) =>
    -
    +
    {isLoading ? (

    Loading...

    ) : filteredOptions.length > 0 ? ( diff --git a/web/components/issues/issue-detail/label/select/root.tsx b/web/components/issues/issue-detail/label/select/root.tsx index c31e1bc612c..de0bcca908a 100644 --- a/web/components/issues/issue-detail/label/select/root.tsx +++ b/web/components/issues/issue-detail/label/select/root.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { TLabelOperations } from "../root"; import { IssueLabelSelect } from "./label-select"; // types -import { TLabelOperations } from "../root"; type TIssueLabelSelectRoot = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/links/create-update-link-modal.tsx b/web/components/issues/issue-detail/links/create-update-link-modal.tsx index fc9eb3838fc..689968f07ce 100644 --- a/web/components/issues/issue-detail/links/create-update-link-modal.tsx +++ b/web/components/issues/issue-detail/links/create-update-link-modal.tsx @@ -152,8 +152,8 @@ export const IssueLinkCreateUpdateModal: FC = (props) ? "Updating Link..." : "Update Link" : isSubmitting - ? "Adding Link..." - : "Add Link"} + ? "Adding Link..." + : "Add Link"}
    diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index f1b003b99b0..4504329f030 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,15 +1,15 @@ import { FC, useState } from "react"; // hooks -import { useIssueDetail, useMember } from "hooks/store"; // ui +import { Pencil, Trash2, LinkIcon } from "lucide-react"; import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { Pencil, Trash2, LinkIcon } from "lucide-react"; // types -import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; export type TIssueLinkDetail = { linkId: string; @@ -50,7 +50,7 @@ export const IssueLinkDetail: FC = (props) => {
    { copyTextToClipboard(linkDetail.url); setToast({ diff --git a/web/components/issues/issue-detail/links/links.tsx b/web/components/issues/issue-detail/links/links.tsx index 368bddb9170..1120c3a5c6c 100644 --- a/web/components/issues/issue-detail/links/links.tsx +++ b/web/components/issues/issue-detail/links/links.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // computed +import { useIssueDetail, useUser } from "hooks/store"; import { IssueLinkDetail } from "./link-detail"; // hooks -import { useIssueDetail, useUser } from "hooks/store"; import { TLinkOperations } from "./root"; export type TLinkOperationsModal = Exclude; @@ -34,6 +34,7 @@ export const IssueLinkList: FC = observer((props) => { issueLinks.length > 0 && issueLinks.map((linkId) => ( ) => Promise; diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 719129d98fe..b65560953d0 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,18 +1,18 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; -import { IssueTitleInput } from "../title-input"; import { IssueDescriptionInput } from "../description-input"; -import { IssueParentDetail } from "./parent"; -import { IssueReaction } from "./reactions"; import { SubIssuesRoot } from "../sub-issues"; +import { IssueTitleInput } from "../title-input"; import { IssueActivity } from "./issue-activity"; +import { IssueParentDetail } from "./parent"; +import { IssueReaction } from "./reactions"; // ui -import { StateGroupIcon } from "@plane/ui"; // types import { TIssueOperations } from "./root"; diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index f0fe06a2e3f..f157ede86d2 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -1,14 +1,13 @@ import React, { useState } from "react"; -import { observer } from "mobx-react-lite"; import xor from "lodash/xor"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { ModuleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -58,14 +57,14 @@ export const IssueModuleSelect: React.FC = observer((props) }; return ( -
    +
    = (props) => { Loading
    ) : subIssueIds && subIssueIds.length > 0 ? ( - subIssueIds.map((issueId) => currentIssue.id != issueId && ) + subIssueIds.map( + (issueId) => currentIssue.id != issueId && + ) ) : (
    No sibling issues diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 2268540bfcc..97c63a017cd 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -1,14 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueCommentReaction = { workspaceSlug: string; @@ -71,15 +70,7 @@ export const IssueCommentReaction: FC = observer((props) else await issueCommentReactionOperations.create(reaction); }, }), - [ - workspaceSlug, - projectId, - commentId, - currentUser, - createCommentReaction, - removeCommentReaction, - userReactions, - ] + [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index a9bc264f395..6f5610634cf 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -1,14 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueReaction = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/reactions/reaction-selector.tsx b/web/components/issues/issue-detail/reactions/reaction-selector.tsx index 0782e7e15e3..655fd91051e 100644 --- a/web/components/issues/issue-detail/reactions/reaction-selector.tsx +++ b/web/components/issues/issue-detail/reactions/reaction-selector.tsx @@ -1,9 +1,9 @@ import { Fragment } from "react"; import { Popover, Transition } from "@headlessui/react"; // helper +import { SmilePlus } from "lucide-react"; import { renderEmoji } from "helpers/emoji.helper"; // icons -import { SmilePlus } from "lucide-react"; const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 26037740640..0fd0902c67f 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -1,15 +1,15 @@ import React from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks +import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { ExistingIssuesListModal } from "components/core"; +import { cn } from "helpers/common.helper"; import { useIssueDetail, useIssues, useProject } from "hooks/store"; // components -import { ExistingIssuesListModal } from "components/core"; // ui -import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types"; @@ -99,7 +99,7 @@ export const IssueRelationSelect: React.FC = observer((pro
    -
    Properties
    +
    Properties
    {/* TODO: render properties using a common component */} -
    -
    -
    +
    +
    +
    State
    @@ -195,7 +199,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId?.toString() ?? ""} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName="text-sm" dropdownArrow @@ -203,8 +207,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Assignees
    @@ -216,7 +220,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { placeholder="Add assignees" multiple buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm justify-between ${ issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" @@ -227,8 +231,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Priority
    @@ -243,8 +247,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Start date
    @@ -259,7 +263,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { maxDate={maxDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon @@ -269,8 +273,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
    -
    -
    +
    +
    Due date
    @@ -285,7 +289,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { minDate={minDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, @@ -299,8 +303,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
    {areEstimatesEnabledForCurrentProject && ( -
    -
    +
    +
    Estimate
    @@ -310,7 +314,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`} placeholder="None" @@ -322,8 +326,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.module_view && ( -
    -
    +
    +
    Module
    @@ -339,8 +343,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.cycle_view && ( -
    -
    +
    +
    Cycle
    @@ -355,13 +359,13 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
    )} -
    -
    +
    +
    Parent
    = observer((props) => { />
    -
    -
    +
    +
    Relates to
    = observer((props) => { />
    -
    -
    +
    +
    Blocking
    = observer((props) => { />
    -
    -
    +
    +
    Blocked by
    = observer((props) => { />
    -
    -
    +
    +
    Duplicate of
    = observer((props) => { />
    -
    -
    +
    +
    Labels
    -
    +
    { const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index cb474d25ec6..fd0153fe89f 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hoks +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export const ModuleCalendarLayout: React.FC = observer(() => { const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index d42a8c5d217..f8933a2271e 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,14 +1,14 @@ +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; +import { EIssueActions } from "../../types"; +import { BaseCalendarRoot } from "../base-calendar-root"; export const CalendarLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 0110aea2bc1..b50efd6c731 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; +import { BaseCalendarRoot } from "../base-calendar-root"; // constants -import { EIssuesStoreType } from "constants/issue"; export interface IViewCalendarLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 5a640a56695..2ce742fe85f 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -4,12 +4,12 @@ import { CalendarDayTile } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarWeek } from "./types"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; 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"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { ICalendarDate, ICalendarWeek } from "./types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 0c8fb377a9e..96887ed6062 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index b23b1998e1e..7b86c16a5a8 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,20 +1,21 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; +import { PlusIcon } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -// components import { ExistingIssuesListModal } from "components/core"; +// ui +// components import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +// components // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -44,7 +45,6 @@ export const CycleEmptyState: React.FC = observer((props) => { const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); - const { updateIssue, fetchIssue } = useIssueDetail(); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index c496cc5fe2d..77b1123b615 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index bf898aec489..b24c4d5d6a0 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { Plus, PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; import emptyProject from "public/empty-state/project.svg"; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index 7a5c6f57f2f..c52d17af54c 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,20 +1,20 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; import { useTheme } from "next-themes"; +import { PlusIcon } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -// components import { ExistingIssuesListModal } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +// ui +// components // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index c7185934c20..e44dd562686 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,15 +1,15 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; import { useTheme } from "next-themes"; // hooks +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index 2da9a826ff9..fd98011fa5f 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; +import { EIssuesStoreType } from "constants/issue"; import { useApplication, useEventTracker } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { EIssuesStoreType } from "constants/issue"; export const ProjectViewEmptyState: React.FC = observer(() => { // store hooks diff --git a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx index 6299bebd7dd..76f36e81580 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { CycleGroupIcon } from "@plane/ui"; import { useCycle } from "hooks/store"; // ui -import { CycleGroupIcon } from "@plane/ui"; // types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx index 891fd6dddeb..fdaed4b9bfa 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -2,10 +2,10 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // helpers +import { DATE_FILTER_OPTIONS } from "constants/filters"; import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // constants -import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 03b0c5138e4..10ad265f36b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -1,9 +1,6 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; -import { useRouter } from "next/router"; // hooks -import { useApplication, useUser } from "hooks/store"; -// components import { AppliedCycleFilters, AppliedDateFilters, @@ -15,12 +12,14 @@ import { AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; -// helpers +import { EUserProjectRoles } from "constants/project"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { useApplication, useUser } from "hooks/store"; +// components +// helpers // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { appliedFilters: IIssueFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/module.tsx b/web/components/issues/issue-layouts/filters/applied-filters/module.tsx index 790383f61a5..e34af843482 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/module.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { DiceIcon } from "@plane/ui"; import { useModule } from "hooks/store"; // ui -import { DiceIcon } from "@plane/ui"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index be3240b5511..aad394d8aa5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { PriorityIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { PriorityIcon } from "@plane/ui"; // types import { TIssuePriorities } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 4c5affe8d4a..24e8fd33848 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 227dc025bcd..35651d8701a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index daa194c9d5e..57e28240b36 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index e9024afeb7b..a075d59d2ec 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index c03e8650474..a431652f14b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -1,18 +1,18 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import isEqual from "lodash/isEqual"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; //ui import { Button } from "@plane/ui"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; +import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; // constants -import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; +import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; +import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; type Props = { globalViewId: string; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 055c32d2068..d2c9ba7ed35 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -1,12 +1,12 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { AppliedFiltersList, SaveFilterView } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 7a6c3933605..91eeef423c1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useIssues, useLabel } from "hooks/store"; import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +import { IIssueFilterOptions } from "@plane/types"; export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 68b5e6727cc..c0b67043a72 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useLabel, useProjectState, useUser } from "hooks/store"; -import { useIssues } from "hooks/store/use-issues"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { useLabel, useProjectState, useUser } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 0768064ec77..278e19d650f 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import isEqual from "lodash/isEqual"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Button } from "@plane/ui"; +import { AppliedFiltersList } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store"; // components -import { AppliedFiltersList } from "components/issues"; // ui -import { Button } from "@plane/ui"; // types import { IIssueFilterOptions } from "@plane/types"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index 620a8f78138..b4a6baa5302 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; import { TStateGroups } from "@plane/types"; type Props = { diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 59a873162ec..fc216afaddf 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; // types import { IState } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index b8988580aaa..02d1b2f04d8 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,8 +11,8 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; type Props = { displayFilters: IIssueDisplayFilterOptions; @@ -37,7 +37,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter); return ( -
    +
    {/* display properties */} {layoutDisplayFiltersOptions?.display_properties && (
    diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index f97140185ec..871bf8ff5f1 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // components +import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; +import { IIssueDisplayProperties } from "@plane/types"; import { FilterHeader } from "../helpers/filter-header"; // types -import { IIssueDisplayProperties } from "@plane/types"; // constants -import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index 0feb1d89190..6de3c940d55 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterOption } from "components/issues"; // types +import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types"; // constants -import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; type Props = { selectedExtraOptions: { diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index a4478e83456..10dfa8c7c47 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants -import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx index 59c83a2003a..9cdcf953b84 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_FILTER_OPTIONS } from "constants/issue"; import { TIssueTypeFilters } from "@plane/types"; // constants -import { ISSUE_FILTER_OPTIONS } from "constants/issue"; type Props = { selectedIssueType: TIssueTypeFilters | undefined; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index e417c650ecd..afcd0ba1b5e 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; import { TIssueOrderByOptions } from "@plane/types"; // constants -import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; type Props = { selectedOrderBy: TIssueOrderByOptions | undefined; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index 198148a8403..98dcb7b95dc 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants -import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index 168e31bc0f3..b26b688afc7 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Avatar, Loader } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,8 +24,8 @@ export const FilterAssignees: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 7bde26ab9fb..45e3309a9bd 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Avatar, Loader } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -22,8 +22,8 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const appliedFiltersCount = appliedFilters?.length ?? 0; diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx index 47b3b05068e..396addde68e 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { observer } from "mobx-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; // components +import { Loader, CycleGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; import { useApplication, useCycle } from "hooks/store"; // ui -import { Loader, CycleGroupIcon } from "@plane/ui"; // types import { TCycleGroups } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index ae7ded8b2d2..257aa197735 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -2,8 +2,6 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // hooks -import { useApplication } from "hooks/store"; -// components import { FilterAssignees, FilterMentions, @@ -18,10 +16,12 @@ import { FilterCycle, FilterModule, } from "components/issues"; +import { ILayoutDisplayFiltersOptions } from "constants/issue"; +import { useApplication } from "hooks/store"; +// components // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { filters: IIssueFilterOptions; diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index b226f42b3d4..42e95553571 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index a6af9833a4e..4d2839b2c86 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader, Avatar } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, Avatar } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,8 +24,8 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx index 49e00f84d44..812cf939f22 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { observer } from "mobx-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; // components +import { Loader, DiceIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; import { useApplication, useModule } from "hooks/store"; // ui -import { Loader, DiceIcon } from "@plane/ui"; type Props = { appliedFilters: string[] | null; diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index 61b7d50c1e4..3bdde6623f9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // hooks +import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx index 2cb7151588e..87def7e29f5 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index ea90971463e..06c1aae9f40 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -2,9 +2,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components +import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // icons -import { StateGroupIcon } from "@plane/ui"; import { STATE_GROUPS } from "constants/state"; // constants diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index c13a69b0a92..5dde1d27980 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, StateGroupIcon } from "@plane/ui"; // types import { IState } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx index b168af668e2..9e0ce18a7a4 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 33b86ada1ff..0d00c36750a 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -1,11 +1,11 @@ import React, { Fragment, useState } from "react"; +import { Placement } from "@popperjs/core"; import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; -import { Placement } from "@popperjs/core"; // ui +import { ChevronUp } from "lucide-react"; import { Button } from "@plane/ui"; // icons -import { ChevronUp } from "lucide-react"; type Props = { children: React.ReactNode; @@ -34,22 +34,26 @@ export const FiltersDropdown: React.FC = (props) => { return ( <> - {menuButton ? : } + {menuButton ? ( + + ) : ( + + )} { // router diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index c7c8e8b0379..3311b6c6ad1 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues, useModule } from "hooks/store"; // components -import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; +import { BaseGanttRoot } from "./base-gantt-root"; export const ModuleGanttLayout: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 18fd3ecef00..1f9e560d3f1 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; +import { BaseGanttRoot } from "./base-gantt-root"; export const GanttLayout: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index 1ed02c2c960..cda2a1e53b3 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -1,14 +1,14 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components +import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; // constants -import { EIssuesStoreType } from "constants/issue"; // types -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; export interface IViewGanttLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 10b73ab355a..94a6243e54f 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -1,22 +1,21 @@ import { useEffect, useState, useRef, FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { createIssuePayload } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { IProject, TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 3951e7032dd..775382f59c5 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -1,30 +1,29 @@ import { FC, useCallback, useRef, useState } from "react"; import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +import { DeleteIssueModal } from "components/issues"; +import { ISSUE_DELETED } from "constants/event-tracker"; +import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useUser } from "hooks/store"; // ui -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; +import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; +import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { IQuickActionProps } from "../list/list-view-types"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { EIssueActions } from "../types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; -import { DeleteIssueModal } from "components/issues"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues } from "hooks/store/use-issues"; import { handleDragDrop } from "./utils"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; -import { ISSUE_DELETED } from "constants/event-tracker"; export interface IBaseKanBanLayout { issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; @@ -227,15 +226,15 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => { if (workspaceSlug && projectId) { - let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; - if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value); - else _kanbanFilters.push(value); + let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; + if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); + else kanbanFilters.push(value); issuesFilter.updateFilters( workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { - [toggle]: _kanbanFilters, + [toggle]: kanbanFilters, }, viewId ); @@ -260,7 +259,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas )}
    @@ -288,7 +287,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
    -
    +
    = memo((props) => { classNames="space-y-2 px-3 py-2" root={scrollableContainerRef} defaultHeight="100px" - horizonatlOffset={50} + horizontalOffset={50} alwaysRender={snapshot.isDragging} pauseHeightUpdateWhileRendering={isDragStarted} changingReference={issueIds} diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 3746111e599..ff1c9287398 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,9 +1,9 @@ import { MutableRefObject, memo } from "react"; //types +import { KanbanIssueBlock } from "components/issues"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; import { EIssueActions } from "../types"; // components -import { KanbanIssueBlock } from "components/issues"; interface IssueBlocksListProps { sub_group_id: string; diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 3cbab589f13..ece578058f7 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,4 +1,7 @@ +import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; +// constants +import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useCycle, @@ -10,9 +13,6 @@ import { useProject, useProjectState, } from "hooks/store"; -// components -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { KanbanGroup } from "./kanban-group"; // types import { GroupByColumnTypes, @@ -25,11 +25,12 @@ import { TUnGroupedIssues, TIssueKanbanFilters, } from "@plane/types"; -// constants +// parent components import { EIssueActions } from "../types"; import { getGroupByColumns } from "../utils"; -import { TCreateModalStoreTypes } from "constants/issue"; -import { MutableRefObject } from "react"; +// components +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { KanbanGroup } from "./kanban-group"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -89,11 +90,19 @@ const GroupByKanBan: React.FC = observer((props) => { const project = useProject(); const label = useLabel(); const cycle = useCycle(); - const _module = useModule(); + const moduleInfo = useModule(); const projectState = useProjectState(); const { peekIssue } = useIssueDetail(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member); + const list = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + moduleInfo, + label, + projectState, + member + ); if (!list) return null; @@ -114,16 +123,19 @@ const GroupByKanBan: React.FC = observer((props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
    +
    {groupList && groupList.length > 0 && groupList.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
    +
    {sub_group_by === null && ( -
    +
    = observer((props) => {
    diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx index ea94647802e..b0859a70d20 100644 --- a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -1,7 +1,7 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Circle, ChevronDown, ChevronUp } from "lucide-react"; // mobx -import { observer } from "mobx-react-lite"; import { TIssueKanbanFilters } from "@plane/types"; interface IHeaderSubGroupByCard { diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index a05fb179110..9d7053216ae 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -3,7 +3,6 @@ import { Droppable } from "@hello-pangea/dnd"; // hooks import { useProjectState } from "hooks/store"; //components -import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; //types import { TGroupedIssues, @@ -14,6 +13,7 @@ import { TUnGroupedIssues, } from "@plane/types"; import { EIssueActions } from "../types"; +import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { groupId: string; diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 20f0cd8e0d7..71a0e661c4d 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -1,20 +1,20 @@ import { useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { createIssuePayload } from "helpers/issue.helper"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; const Inputs = (props: any) => { const { register, setFocus, projectDetail } = props; @@ -139,7 +139,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser return ( <> {isOpen ? ( -
    +
    { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={CycleIssueQuickActions} viewId={cycleId?.toString() ?? ""} storeType={EIssuesStoreType.CYCLE} diff --git a/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx index 9152dbfe576..50173413429 100644 --- a/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/draft-issue-root.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export interface IKanBanLayout {} @@ -42,7 +42,7 @@ export const DraftKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} /> ); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 07ad7eb83ca..96cfaceda89 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hook +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleKanBanLayout {} @@ -52,7 +52,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={ModuleIssueQuickActions} viewId={moduleId?.toString()} storeType={EIssuesStoreType.MODULE} diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index c6c04165447..99d703a722c 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; +import { useMemo } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export const ProfileIssuesKanBanLayout: React.FC = observer(() => { const router = useRouter(); @@ -55,7 +55,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROFILE} canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject} diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index efd86bc8e94..432663a02de 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { useMemo } from "react"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store/use-issues"; // components -import { ProjectIssueQuickActions } from "components/issues"; -import { BaseKanBanRoot } from "../base-kanban-root"; // types import { TIssue } from "@plane/types"; // constants import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseKanBanRoot } from "../base-kanban-root"; export interface IKanBanLayout {} @@ -46,7 +46,7 @@ export const KanBanLayout: React.FC = observer(() => { issueActions={issueActions} issues={issues} issuesFilter={issuesFilter} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT} /> diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 8dd33b72844..77689e563c5 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // constant -import { EIssuesStoreType } from "constants/issue"; // types import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; export interface IViewKanBanLayout { issueActions: { @@ -34,7 +34,7 @@ export const ProjectViewKanBanLayout: React.FC = observer((pr issueActions={issueActions} issuesFilter={issuesFilter} issues={issues} - showLoader={true} + showLoader QuickActions={ProjectIssueQuickActions} storeType={EIssuesStoreType.PROJECT_VIEW} viewId={viewId?.toString()} diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index d60e3b61868..75cb830c6a2 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -1,10 +1,8 @@ import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; // components -import { KanBan } from "./default"; -import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; -import { HeaderGroupByCard } from "./headers/group-by-card"; -// types +import { TCreateModalStoreTypes } from "constants/issue"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { GroupByColumnTypes, IGroupByColumn, @@ -16,11 +14,13 @@ import { TUnGroupedIssues, TIssueKanbanFilters, } from "@plane/types"; -// constants import { EIssueActions } from "../types"; -import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { getGroupByColumns } from "../utils"; -import { TCreateModalStoreTypes } from "constants/issue"; +import { KanBan } from "./default"; +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { HeaderSubGroupByCard } from "./headers/sub-group-by-card"; +// types +// constants interface ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; @@ -47,7 +47,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ kanbanFilters, handleKanbanFilters, }) => ( -
    +
    {list && list.length > 0 && list.map((_list: IGroupByColumn) => ( @@ -129,7 +129,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { {list && list.length > 0 && list.map((_list: any) => ( -
    +
    = observer((props) => { const project = useProject(); const label = useLabel(); const cycle = useCycle(); - const _module = useModule(); + const projectModule = useModule(); const projectState = useProjectState(); const groupByList = getGroupByColumns( group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member @@ -243,7 +243,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { sub_group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index ffe9de66142..8a3d87e403b 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,23 +1,23 @@ -import { List } from "./default"; import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; // types -import { TIssue } from "@plane/types"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { TCreateModalStoreTypes } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useIssues, useUser } from "hooks/store"; +import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; +import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; -import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; // components +import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { TCreateModalStoreTypes } from "constants/issue"; // hooks -import { useIssues, useUser } from "hooks/store"; interface IBaseListRoot { issuesFilter: diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 90fee10cc73..a2148634c59 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; // components -import { IssueProperties } from "../properties/all-properties"; // hooks -import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui import { Spinner, Tooltip, ControlLink } from "@plane/ui"; // helper import { cn } from "helpers/common.helper"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; +import { IssueProperties } from "../properties/all-properties"; import { EIssueActions } from "../types"; interface IssueBlockProps { diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index d3c8d14061e..23c364b675d 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -1,10 +1,10 @@ import { FC, MutableRefObject } from "react"; // components +import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { EIssueActions } from "../types"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index c6f82c2be94..db1bcb06a02 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,9 +1,10 @@ import { useRef } from "react"; // components import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; -import { HeaderGroupByCard } from "./headers/group-by-card"; // hooks +import { TCreateModalStoreTypes } from "constants/issue"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; +// constants // types import { GroupByColumnTypes, @@ -15,9 +16,8 @@ import { IGroupByColumn, } from "@plane/types"; import { EIssueActions } from "../types"; -// constants -import { TCreateModalStoreTypes } from "constants/issue"; import { getGroupByColumns } from "../utils"; +import { HeaderGroupByCard } from "./headers/group-by-card"; export interface IGroupByList { issueIds: TGroupedIssues | TUnGroupedIssues | any; @@ -66,7 +66,7 @@ const GroupByList: React.FC = (props) => { const label = useLabel(); const projectState = useProjectState(); const cycle = useCycle(); - const _module = useModule(); + const projectModule = useModule(); const containerRef = useRef(null); @@ -74,7 +74,7 @@ const GroupByList: React.FC = (props) => { group_by as GroupByColumnTypes, project, cycle, - _module, + projectModule, label, projectState, member, @@ -119,7 +119,7 @@ const GroupByList: React.FC = (props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
    +
    {groups && groups.length > 0 && groups.map( diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 404107af4a0..7edf89bf1a3 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,19 +1,19 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components -import { CreateUpdateIssueModal } from "components/issues"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal } from "components/issues"; // ui -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // mobx -import { observer } from "mobx-react-lite"; // hooks +import { TCreateModalStoreTypes } from "constants/issue"; import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; -import { useState } from "react"; -import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { icon?: React.ReactNode; diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 3c71293b424..7bae7ecff14 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -1,20 +1,20 @@ -import { FC, useEffect, useState, useRef, use } from "react"; +import { FC, useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui -import { setPromiseToast } from "@plane/ui"; // types import { TIssue, IProject } from "@plane/types"; // helper -import { createIssuePayload } from "helpers/issue.helper"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 6e70d00d029..2f3807beb21 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ArchivedIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ArchivedIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants -import { BaseListRoot } from "../base-list-root"; import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; export const ArchivedIssueListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 5c15ebe602e..46ee7f32ecb 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,16 +1,16 @@ import React, { useCallback, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components -import { CycleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants -import { BaseListRoot } from "../base-list-root"; import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; export interface ICycleListLayout {} diff --git a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx index e11971874a9..10b75b115fc 100644 --- a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export const DraftIssueListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 95c62d34cdb..aca528a6a6a 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleListLayout {} diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index fa4a05bbcbb..dc0c68cd810 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,17 +1,17 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; export const ProfileIssuesListLayout: FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 9e1b5830b70..8a0935979be 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,16 +1,16 @@ import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ProjectIssueQuickActions } from "components/issues"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 5ecfd6da28e..82ca03d4279 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // constants -import { EIssuesStoreType } from "constants/issue"; // types -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +import { EIssueActions } from "../../types"; // components import { BaseListRoot } from "../base-list-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; export interface IViewListLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 238d2e74457..8b3e2e67335 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,14 +1,10 @@ import { useCallback, useMemo } from "react"; +import xor from "lodash/xor"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; -import xor from "lodash/xor"; // hooks -import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; -// components -import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { DateDropdown, EstimateDropdown, @@ -18,15 +14,19 @@ import { CycleDropdown, StateDropdown, } from "components/dropdowns"; -// helpers +import { ISSUE_UPDATED } from "constants/event-tracker"; +import { EIssuesStoreType } from "constants/issue"; +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; -// types +import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; +// components import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; +import { IssuePropertyLabels } from "../properties/labels"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// helpers +// types // constants -import { ISSUE_UPDATED } from "constants/event-tracker"; -import { EIssuesStoreType } from "constants/issue"; export interface IIssueProperties { issue: TIssue; @@ -338,7 +338,7 @@ export const IssueProperties: React.FC = observer((props) => { disabled={isReadOnly} multiple buttonVariant="border-with-text" - showCount={true} + showCount showTooltip />
    diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 0c1091d39dd..a57c60d6f97 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,16 +1,16 @@ import { Fragment, useEffect, useRef, useState } from "react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search, Tags } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; import { useApplication, useLabel } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { Combobox } from "@headlessui/react"; -import { Tooltip } from "@plane/ui"; // types -import { Placement } from "@popperjs/core"; import { IIssueLabel } from "@plane/types"; export interface IIssuePropertyLabels { @@ -56,7 +56,7 @@ export const IssuePropertyLabels: React.FC = observer((pro // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); // store hooks const { router: { workspaceSlug }, @@ -149,7 +149,7 @@ export const IssuePropertyLabels: React.FC = observer((pro {projectLabels ?.filter((l) => value.includes(l?.id)) .map((label) => ( - +
    = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index a30db3a8295..dae88a3874b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -2,17 +2,19 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react"; // hooks -import { useEventTracker, useIssues, useUser } from "hooks/store"; -// ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; -// components + import { DeleteIssueModal } from "components/issues"; -// helpers +// ui +// components +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssues, useUser } from "hooks/store"; +// components +// helpers // types import { IQuickActionProps } from "../list/list-view-types"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueQuickActions: React.FC = (props) => { const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 2b4a5fa05c9..89beda00c99 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,24 +1,25 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; // ui +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // icons -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -// helpers +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; +// components +// helpers // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { STATE_GROUPS } from "constants/state"; export const CycleIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index cf090385dfa..26eb6997cf4 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,23 +1,24 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -// helpers +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; import { copyUrlToClipboard } from "helpers/string.helper"; +import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; +// components +// helpers // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { STATE_GROUPS } from "constants/state"; export const ModuleIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 7afbd24210a..33b73f88cfa 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,23 +1,23 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import omit from "lodash/omit"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; -import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; // components -import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constant -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; export const ProjectIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 3b098c8a106..84101542fa3 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,31 +1,31 @@ import React, { Fragment, useCallback, useMemo } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import isEmpty from "lodash/isEmpty"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; -import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; -// components +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; +import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +// components // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { workspaceSlug, globalViewId, ...routeFilters } = router.query; // theme const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties @@ -61,14 +61,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { globalViewId && ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) ) { - const routerQueryParams = { ...router.query }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams; - let issueFilters: any = {}; - Object.keys(filters).forEach((key) => { + Object.keys(routeFilters).forEach((key) => { const filterKey: any = key; - const filterValue = filters[key]?.toString() || undefined; + const filterValue = routeFilters[key]?.toString() || undefined; if ( ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) && filterKey && @@ -77,7 +73,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; }); - if (!isEmpty(filters)) + if (!isEmpty(routeFilters)) updateFilters( workspaceSlug.toString(), undefined, diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 7db9a1e3b77..ae8ca400acc 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -1,9 +1,8 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { ArchivedIssueListLayout, @@ -11,9 +10,10 @@ import { ProjectArchivedEmptyState, IssuePeekOverview, } from "components/issues"; +import { ListLayoutLoader } from "components/ui"; import { EIssuesStoreType } from "constants/issue"; // ui -import { ListLayoutLoader } from "components/ui"; +import { useIssues } from "hooks/store"; export const ArchivedIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 759495284be..5f308fbd17a 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -1,12 +1,12 @@ import React, { Fragment, useState } from "react"; -import { useRouter } from "next/router"; +import isEmpty from "lodash/isEmpty"; +import size from "lodash/size"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; -import size from "lodash/size"; -import isEmpty from "lodash/isEmpty"; // hooks -import { useCycle, useIssues } from "hooks/store"; // components +import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { CycleAppliedFiltersRoot, CycleCalendarLayout, @@ -17,10 +17,10 @@ import { CycleSpreadsheetLayout, IssuePeekOverview, } from "components/issues"; -import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useCycle, useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 02b666ceb86..1a1602ad1be 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -1,19 +1,19 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +import { IssuePeekOverview } from "components/issues/peek-overview"; +import { ActiveLoader } from "components/ui"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components +import { ProjectDraftEmptyState } from "../empty-states"; import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; +import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; -import { ProjectDraftEmptyState } from "../empty-states"; -import { IssuePeekOverview } from "components/issues/peek-overview"; -import { ActiveLoader } from "components/ui"; // ui -import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; // constants -import { EIssuesStoreType } from "constants/issue"; export const DraftIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 14505c65aa5..0c6ba3b66ab 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -1,10 +1,9 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; +import size from "lodash/size"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; -import size from "lodash/size"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -19,6 +18,7 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index cae73610efa..a57d73b2c77 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -1,8 +1,10 @@ import { FC, Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // components +// ui +import { Spinner } from "@plane/ui"; import { ListLayout, CalendarLayout, @@ -13,14 +15,12 @@ import { ProjectEmptyState, IssuePeekOverview, } from "components/issues"; -// ui -import { Spinner } from "@plane/ui"; // hooks -import { useIssues } from "hooks/store"; // helpers import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; export const ProjectLayoutRoot: FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index fa942b7f604..dbd6c5f96a7 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,9 +1,8 @@ import React, { Fragment, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -18,6 +17,7 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 2f09b55d601..5a522a527e4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -1,21 +1,21 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { EIssueFilterType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useUser } from "hooks/store"; // views -import { SpreadsheetView } from "./spreadsheet-view"; // types -import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; -import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EUserProjectRoles } from "constants/project"; import { ICycleIssuesFilter, ICycleIssues } from "store/issue/cycle"; import { IModuleIssuesFilter, IModuleIssues } from "store/issue/module"; import { IProjectIssuesFilter, IProjectIssues } from "store/issue/project"; import { IProjectViewIssuesFilter, IProjectViewIssues } from "store/issue/project-views"; -import { EIssueFilterType } from "constants/issue"; +import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; +import { IQuickActionProps } from "../list/list-view-types"; +import { EIssueActions } from "../types"; +import { SpreadsheetView } from "./spreadsheet-view"; interface IBaseSpreadsheetRoot { issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -90,7 +90,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { viewId ); }, - [issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId] + [issueFiltersStore, projectId, workspaceSlug, viewId] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx index 88fbf105461..658e9c79be3 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx @@ -1,14 +1,14 @@ import React, { useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleDropdown } from "components/dropdowns"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker, useIssues } from "hooks/store"; // components -import { CycleDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index e261797af83..adc4a971b12 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -2,13 +2,13 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { CalendarCheck2 } from "lucide-react"; // hooks -import { useProjectState } from "hooks/store"; // components import { DateDropdown } from "components/dropdowns"; // helpers +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; +import { useProjectState } from "hooks/store"; // types import { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index f7a472b49f9..8143be21465 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,6 +1,6 @@ // components -import { EstimateDropdown } from "components/dropdowns"; import { observer } from "mobx-react-lite"; +import { EstimateDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx index ac06525dff6..6c59c22af00 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -1,5 +1,4 @@ //ui -import { CustomMenu } from "@plane/ui"; import { ArrowDownWideNarrow, ArrowUpNarrowWide, @@ -9,12 +8,13 @@ import { ListFilter, MoveRight, } from "lucide-react"; +import { CustomMenu } from "@plane/ui"; //hooks +import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; import useLocalStorage from "hooks/use-local-storage"; //types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types"; //constants -import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; interface Props { property: keyof IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index 60e429c9fc8..1e6ae197af5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -1,11 +1,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; // components -import { IssuePropertyLabels } from "../../properties"; // hooks import { useLabel } from "hooks/store"; // types import { TIssue } from "@plane/types"; +import { IssuePropertyLabels } from "../../properties"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx index c688c6e1d20..67c72d2a838 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/module-column.tsx @@ -1,15 +1,15 @@ import React, { useCallback } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import xor from "lodash/xor"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ModuleDropdown } from "components/dropdowns"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker, useIssues } from "hooks/store"; // components -import { ModuleDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type Props = { issue: TIssue; @@ -71,7 +71,7 @@ export const SpreadsheetModuleColumn: React.FC = observer((props) => { buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5" onClose={onClose} multiple - showCount={true} + showCount showTooltip />
    diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index b8801559c5a..714134d0c0b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -8,7 +8,7 @@ import { TIssue } from "@plane/types"; type Props = { issue: TIssue; onClose: () => void; - onChange: (issue: TIssue, data: Partial,updates:any) => void; + onChange: (issue: TIssue, data: Partial, updates: any) => void; disabled: boolean; }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index c635ca85e85..85e294641b5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; // types import { TIssue } from "@plane/types"; // helpers -import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 3ce70868de8..01be9fe994b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -1,14 +1,14 @@ import { useRef } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // types +import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; +import { useEventTracker } from "hooks/store"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { EIssueActions } from "../types"; // constants -import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; // components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import { useEventTracker } from "hooks/store"; -import { observer } from "mobx-react"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index abf6c3a0149..161aa07aec0 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -1,24 +1,25 @@ import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // icons import { ChevronRight, MoreHorizontal } from "lucide-react"; -// constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; -// components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; -import { IssueColumn } from "./issue-column"; // ui import { ControlLink, Tooltip } from "@plane/ui"; -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useIssueDetail, useProject } from "hooks/store"; +// components +import RenderIfVisible from "components/core/render-if-visible-HOC"; +// constants +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useIssueDetail, useProject } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssueDisplayProperties, TIssue } from "@plane/types"; +// local components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { EIssueActions } from "../types"; +import { IssueColumn } from "./issue-column"; interface Props { displayProperties: IIssueDisplayProperties; @@ -255,6 +256,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { {/* Rest of the columns */} {SPREADSHEET_PROPERTY_LIST.map((property) => ( { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index af8abc80159..c52b40527d9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -2,13 +2,13 @@ import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssuesStoreType } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index 4ce54cff5a1..cc570fd81b5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -2,13 +2,13 @@ import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssuesStoreType } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ProjectSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index d8b7571e584..dd134e070c2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -2,15 +2,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -// types import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +// types // constants -import { EIssuesStoreType } from "constants/issue"; export interface IViewSpreadsheetLayout { issueActions: { diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx index 4401eb839a9..346846defef 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx @@ -1,10 +1,10 @@ import { useRef } from "react"; //types +import { observer } from "mobx-react"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; //components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { HeaderColumn } from "./columns/header-column"; -import { observer } from "mobx-react"; interface Props { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index 98666d79011..ea0e0f1c2c8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -1,9 +1,9 @@ // ui import { LayersIcon } from "@plane/ui"; // types +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; // constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column"; @@ -38,6 +38,7 @@ export const SpreadsheetHeader = (props: Props) => { {SPREADSHEET_PROPERTY_LIST.map((property) => ( { // states const isScrolled = useRef(false); - const handleScroll = () => { + const handleScroll = useCallback(() => { if (!containerRef.current) return; const scrollLeft = containerRef.current.scrollLeft; @@ -51,19 +51,19 @@ export const SpreadsheetTable = observer((props: Props) => { //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly if (scrollLeft > 0 !== isScrolled.current) { - const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); + const firstColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); - for (let i = 0; i < firtColumns.length; i++) { + for (let i = 0; i < firstColumns.length; i++) { const shadow = i === 0 ? headerShadow : columnShadow; if (scrollLeft > 0) { - (firtColumns[i] as HTMLElement).style.boxShadow = shadow; + (firstColumns[i] as HTMLElement).style.boxShadow = shadow; } else { - (firtColumns[i] as HTMLElement).style.boxShadow = "none"; + (firstColumns[i] as HTMLElement).style.boxShadow = "none"; } } isScrolled.current = scrollLeft > 0; } - }; + }, [containerRef]); useEffect(() => { const currentContainerRef = containerRef.current; @@ -73,7 +73,7 @@ export const SpreadsheetTable = observer((props: Props) => { return () => { if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll); }; - }, []); + }, [handleScroll, containerRef]); const handleKeyBoardNavigation = useTableKeyboardNavigation(); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index e7b2bcee609..f71634ab841 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -3,12 +3,12 @@ import { observer } from "mobx-react-lite"; // components import { Spinner } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues"; -import { SpreadsheetTable } from "./spreadsheet-table"; -// types +import { useProject } from "hooks/store"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { EIssueActions } from "../types"; +import { SpreadsheetTable } from "./spreadsheet-table"; +// types //hooks -import { useProject } from "hooks/store"; type Props = { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index ce49d774db1..6dd462fd133 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,19 +1,19 @@ +import { ContrastIcon } from "lucide-react"; import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; // stores +import { ISSUE_PRIORITIES } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { renderEmoji } from "helpers/emoji.helper"; +import { ICycleStore } from "store/cycle.store"; +import { ILabelStore } from "store/label.store"; import { IMemberRootStore } from "store/member"; +import { IModuleStore } from "store/module.store"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; -import { ILabelStore } from "store/label.store"; -import { ICycleStore } from "store/cycle.store"; -import { IModuleStore } from "store/module.store"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // constants -import { STATE_GROUPS } from "constants/state"; -import { ISSUE_PRIORITIES } from "constants/issue"; // types import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types"; -import { ContrastIcon } from "lucide-react"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, @@ -62,7 +62,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined return { id: project.id, name: project.name, - icon:
    {renderEmoji(project.emoji || "")}
    , + icon:
    {renderEmoji(project.emoji || "")}
    , payload: { project_id: project.id }, }; }) as any; @@ -112,19 +112,19 @@ const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore const modules = []; moduleIds.map((moduleId) => { - const _module = getModuleById(moduleId); - if (_module) + const moduleInfo = getModuleById(moduleId); + if (moduleInfo) modules.push({ - id: _module.id, - name: _module.name, - icon: , - payload: { module_ids: [_module.id] }, + id: moduleInfo.id, + name: moduleInfo.name, + icon: , + payload: { module_ids: [moduleInfo.id] }, }); }) as any; modules.push({ id: "None", name: "None", - icon: , + icon: , }); return modules as any; @@ -138,7 +138,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine id: state.id, name: state.name, icon: ( -
    +
    ), @@ -153,7 +153,7 @@ const getStateGroupColumns = () => { id: stateGroup.key, name: stateGroup.label, icon: ( -
    +
    ), @@ -183,7 +183,7 @@ const getLabelsColumns = (label: ILabelStore) => { id: label.id, name: label.name, icon: ( -
    +
    ), payload: label?.id === "None" ? {} : { label_ids: [label.id] }, })); diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index b4dae211d7d..785ccb0bbc1 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -1,15 +1,15 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { ConfirmIssueDiscard } from "components/issues"; +import { IssueFormRoot } from "components/issues/issue-modal/form"; import { useEventTracker } from "hooks/store"; // services import { IssueDraftService } from "services/issue"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { IssueFormRoot } from "components/issues/issue-modal/form"; -import { ConfirmIssueDiscard } from "components/issues"; // types import type { TIssue } from "@plane/types"; diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 7fcb6cffa62..527ebd0e183 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,20 +1,13 @@ import React, { FC, useState, useRef, useEffect, Fragment } from "react"; -import { useRouter } from "next/router"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { LayoutPanelTop, Sparkle, X } from "lucide-react"; // editor -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks -import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; -// services -import { AIService } from "services/ai.service"; -import { FileService } from "services/file.service"; -// components +import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { GptAssistantPopover } from "components/core"; -import { ParentIssuesListModal } from "components/issues"; -import { IssueLabelSelect } from "components/issues/select"; -import { CreateLabelModal } from "components/labels"; import { CycleDropdown, DateDropdown, @@ -25,10 +18,17 @@ import { MemberDropdown, StateDropdown, } from "components/dropdowns"; +import { ParentIssuesListModal } from "components/issues"; +import { IssueLabelSelect } from "components/issues/select"; +import { CreateLabelModal } from "components/labels"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; +// services +import { AIService } from "services/ai.service"; +import { FileService } from "services/file.service"; +// components // ui -import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -360,14 +360,14 @@ export const IssueFormRoot: FC = observer((props) => { ref={ref} hasError={Boolean(errors.name)} placeholder="Issue Title" - className="resize-none text-xl w-full" + className="w-full resize-none text-xl" tabIndex={getTabIndex("name")} /> )} />
    {data?.description_html === undefined ? ( - +
    @@ -381,18 +381,18 @@ export const IssueFormRoot: FC = observer((props) => {
    -
    +
    ) : ( -
    +
    {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index b97eafc0643..f5b804e74f6 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -1,17 +1,15 @@ import React, { useEffect, useState } from "react"; - import { useRouter } from "next/router"; - // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services +import { Rocket, Search } from "lucide-react"; +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import useDebounce from "hooks/use-debounce"; import { ProjectService } from "services/project"; // hooks -import useDebounce from "hooks/use-debounce"; // ui -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // icons -import { Rocket, Search } from "lucide-react"; // types import { ISearchIssueResponse } from "@plane/types"; diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8d8ec00df3b..b47551bc632 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui import { @@ -13,15 +13,17 @@ import { TOAST_TYPE, setToast, } from "@plane/ui"; +// components +import { IssueSubscription, IssueUpdateStatus } from "components/issues"; +import { STATE_GROUPS } from "constants/state"; // helpers +import { cn } from "helpers/common.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // store hooks import { useIssueDetail, useProjectState, useUser } from "hooks/store"; // helpers -import { cn } from "helpers/common.helper"; // components -import { IssueSubscription, IssueUpdateStatus } from "components/issues"; -import { STATE_GROUPS } from "constants/state"; +// helpers export type TPeekModes = "side-peek" | "modal" | "full-screen"; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 7f540874c76..59b1c160905 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,14 +1,14 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // store hooks +import { TIssueOperations } from "components/issues"; import { useIssueDetail, useProject, useUser } from "hooks/store"; // hooks import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { TIssueOperations } from "components/issues"; +import { IssueDescriptionInput } from "../description-input"; import { IssueReaction } from "../issue-detail/reactions"; import { IssueTitleInput } from "../title-input"; -import { IssueDescriptionInput } from "../description-input"; interface IPeekOverviewIssueDetails { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 2f5a02c11e7..8ae021b8672 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -12,9 +12,9 @@ import { CalendarCheck2, } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui"; +import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { IssueLinkRoot, IssueCycleSelect, @@ -24,12 +24,12 @@ import { TIssueOperations, IssueRelationSelect, } from "components/issues"; -import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // components +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers -import { cn } from "helpers/common.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; interface IPeekOverviewProperties { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index b28cc5de60f..3eae8d3e870 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -1,18 +1,19 @@ import { FC, useEffect, useState, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -// ui import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -// components import { IssueView } from "components/issues"; +// ui +// components +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; +// components // types import { TIssue } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index f94901c454f..aa7bd395fd5 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -1,12 +1,7 @@ import { FC, useRef, useState } from "react"; - import { observer } from "mobx-react-lite"; - -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useKeypress from "hooks/use-keypress"; -// store hooks -import { useIssueDetail } from "hooks/store"; +// ui +import { Spinner } from "@plane/ui"; // components import { DeleteIssueModal, @@ -17,9 +12,12 @@ import { TIssueOperations, ArchiveIssueModal, } from "components/issues"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useKeypress from "hooks/use-keypress"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// store hooks import { IssueActivity } from "../issue-detail/issue-activity"; -// ui -import { Spinner } from "@plane/ui"; interface IIssueView { workspaceSlug: string; @@ -139,7 +137,7 @@ export const IssueView: FC = observer((props) => { disabled={disabled} /> {/* content */} -
    +
    {isLoading && !issue ? (
    @@ -170,7 +168,7 @@ export const IssueView: FC = observer((props) => {
    ) : ( -
    +
    >; diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index a748e986e9b..5d7d197308b 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -1,16 +1,16 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // components +import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; +import { TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { IssueProperty } from "./properties"; // ui -import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; -import { observer } from "mobx-react-lite"; export interface ISubIssues { workspaceSlug: string; diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index ad09938cb23..cb1d66461a1 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TIssue } from "@plane/types"; import { IssueListItem } from "./issue-list-item"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; export interface IIssueList { diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 03c9d8902d1..f737b57e77f 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -1,8 +1,8 @@ import React from "react"; // hooks +import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { useIssueDetail } from "hooks/store"; // components -import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // types import { TSubIssueOperations } from "./root"; diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index da49200dd07..ed46a40f55c 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -1,20 +1,20 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; // hooks -import { useEventTracker, useIssueDetail } from "hooks/store"; -// components +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssueDetail } from "hooks/store"; +// components +import { IUser, TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { ProgressBar } from "./progressbar"; // ui -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IUser, TIssue } from "@plane/types"; export interface ISubIssuesRoot { workspaceSlug: string; diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 2db4eb4b5ff..bb412b795bc 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react"; // components import { TextArea } from "@plane/ui"; // types +import useDebounce from "hooks/use-debounce"; import { TIssueOperations } from "./issue-detail"; // hooks -import useDebounce from "hooks/use-debounce"; export type IssueTitleInputProps = { disabled?: boolean; diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index b6a3f63e87d..ee098874164 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; import { Dialog, Popover, Transition } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { useLabel } from "hooks/store"; // ui -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel, IState } from "@plane/types"; // constants -import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types type Props = { diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index d30d48a6ac9..a29a334b608 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -1,17 +1,17 @@ import React, { forwardRef, useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; -// hooks -import { useLabel } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; +// hooks +import { useLabel } from "hooks/store"; // types import { IIssueLabel } from "@plane/types"; -// fetch-keys -import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { labelForm: boolean; @@ -74,6 +74,7 @@ export const CreateUpdateLabelInline = observer( const handleLabelUpdate: SubmitHandler = async (formData) => { if (!workspaceSlug || !projectId || isSubmitting) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) .then(() => { reset(defaultValues); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index 83b3e807d88..d5c269136fc 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // hooks +import { AlertTriangle } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useLabel } from "hooks/store"; // icons -import { AlertTriangle } from "lucide-react"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IIssueLabel } from "@plane/types"; diff --git a/web/components/labels/label-block/label-item-block.tsx b/web/components/labels/label-block/label-item-block.tsx index eca3bcaafd8..2a797d0b63c 100644 --- a/web/components/labels/label-block/label-item-block.tsx +++ b/web/components/labels/label-block/label-item-block.tsx @@ -1,12 +1,12 @@ import { useRef, useState } from "react"; -import { LucideIcon, X } from "lucide-react"; import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd"; +import { LucideIcon, X } from "lucide-react"; //ui import { CustomMenu } from "@plane/ui"; //types +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { IIssueLabel } from "@plane/types"; //hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; //components import { DragHandle } from "./drag-handle"; import { LabelName } from "./label-name"; diff --git a/web/components/labels/project-setting-label-group.tsx b/web/components/labels/project-setting-label-group.tsx index 71d11dacb91..6519e581e47 100644 --- a/web/components/labels/project-setting-label-group.tsx +++ b/web/components/labels/project-setting-label-group.tsx @@ -1,12 +1,4 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { Disclosure, Transition } from "@headlessui/react"; - -// store -import { observer } from "mobx-react-lite"; -// icons -import { ChevronDown, Pencil, Trash2 } from "lucide-react"; -// types -import { IIssueLabel } from "@plane/types"; import { Draggable, DraggableProvided, @@ -14,10 +6,18 @@ import { DraggableStateSnapshot, Droppable, } from "@hello-pangea/dnd"; -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; +import { observer } from "mobx-react-lite"; +import { Disclosure, Transition } from "@headlessui/react"; + +// store +// icons +import { ChevronDown, Pencil, Trash2 } from "lucide-react"; +// types +import useDraggableInPortal from "hooks/use-draggable-portal"; +import { IIssueLabel } from "@plane/types"; import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { ProjectSettingLabelItem } from "./project-setting-label-item"; -import useDraggableInPortal from "hooks/use-draggable-portal"; type Props = { label: IIssueLabel; @@ -107,7 +107,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { customMenuItems={customMenuItems} dragHandleProps={dragHandleProps} handleLabelDelete={handleLabelDelete} - isLabelGroup={true} + isLabelGroup /> )} diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx index ed72e4503d9..30e424064df 100644 --- a/web/components/labels/project-setting-label-item.tsx +++ b/web/components/labels/project-setting-label-item.tsx @@ -1,14 +1,14 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { useRouter } from "next/router"; import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { useRouter } from "next/router"; import { X, Pencil } from "lucide-react"; // hooks import { useLabel } from "hooks/store"; // types import { IIssueLabel } from "@plane/types"; // components -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; type Props = { label: IIssueLabel; diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index fcd84d70ab1..ba6b43b0bf1 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,6 +1,4 @@ import React, { useState, useRef } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { DragDropContext, Draggable, @@ -9,24 +7,26 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks -import { useLabel, useUser } from "hooks/store"; -import useDraggableInPortal from "hooks/use-draggable-portal"; -// components +import { Button, Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { useLabel, useUser } from "hooks/store"; +import useDraggableInPortal from "hooks/use-draggable-portal"; +// components // ui -import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -76,16 +76,18 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { if (destination?.droppableId === LABELS_ROOT) parentLabel = null; if (result.reason == "DROP" && childLabel != parentLabel) { - updateLabelPosition( - workspaceSlug?.toString()!, - projectId?.toString()!, - childLabel, - parentLabel, - index, - prevParentLabel == parentLabel, - prevIndex - ); - return; + if (workspaceSlug && projectId) { + updateLabelPosition( + workspaceSlug?.toString(), + projectId?.toString(), + childLabel, + parentLabel, + index, + prevParentLabel == parentLabel, + prevIndex + ); + return; + } } }; @@ -104,7 +106,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
    {showLabelForm && ( -
    +
    { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
    +
    = observer((props) => { }); }; - const handleUpdateModule = async (payload: Partial, dirtyFields: any) => { + const handleUpdateModule = async (payload: Partial, dirtyFields: unknown) => { if (!workspaceSlug || !projectId || !data) return; const selectedProjectId = payload.project_id ?? projectId.toString(); @@ -92,7 +92,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); captureModuleEvent({ eventName: MODULE_UPDATED, - payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" }, + payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" }, }); }) .catch((err) => { @@ -108,7 +108,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }; - const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: unknown) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index dbbde56d785..8023657da4a 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -1,21 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // hooks +import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { MODULE_STATUS } from "constants/module"; +import { EUserProjectRoles } from "constants/project"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; // components -import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui -import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; // constants -import { MODULE_STATUS } from "constants/module"; -import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; type Props = { moduleId: string; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 63e780cb2e4..7fe25b918d4 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -1,13 +1,9 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // hooks -import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; -// components -import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; -// ui import { Avatar, AvatarGroup, @@ -18,13 +14,17 @@ import { setToast, setPromiseToast, } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; +// components +// ui +// helpers +// constants type Props = { moduleId: string; @@ -175,9 +175,9 @@ export const ModuleListItem: React.FC = observer((props) => { )} setDeleteModal(false)} /> -
    -
    -
    +
    +
    +
    @@ -202,10 +202,10 @@ export const ModuleListItem: React.FC = observer((props) => {
    -
    +
    {moduleStatus && ( = observer((props) => {
    -
    +
    {renderDate && ( @@ -226,7 +226,7 @@ export const ModuleListItem: React.FC = observer((props) => { )}
    -
    +
    {moduleDetails.member_ids.length > 0 ? ( diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index e3f50447967..4763639ed58 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -1,12 +1,12 @@ -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { useCallback, useState } from "react"; +import router from "next/router"; +import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { CustomMenu } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store"; -import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import router from "next/router"; -import { useCallback, useState } from "react"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ModuleMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); @@ -83,35 +83,36 @@ export const ModuleMobileHeader = () => { onClose={() => setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} /> -
    +
    Layout} + customButton={Layout} customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" closeOnSelect > {layouts.map((layout, index) => ( { handleLayoutChange(ISSUE_LAYOUTS[index].key); }} className="flex items-center gap-2" > - +
    {layout.title}
    ))}
    -
    +
    + Filters - + } > @@ -127,14 +128,14 @@ export const ModuleMobileHeader = () => { />
    -
    +
    + Display - + } > @@ -153,7 +154,7 @@ export const ModuleMobileHeader = () => { diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index 81614b61ba7..5590d0390bc 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useModule } from "hooks/store"; // components diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index bf12fde8bbd..33c11cbd88b 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // components -import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { EUserProjectRoles } from "constants/project"; import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; export const ModulesListView: React.FC = observer(() => { // router diff --git a/web/components/modules/select/status.tsx b/web/components/modules/select/status.tsx index 33a634e9ba1..8efdcb472f5 100644 --- a/web/components/modules/select/status.tsx +++ b/web/components/modules/select/status.tsx @@ -5,9 +5,9 @@ import { Controller, FieldError, Control } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon, ModuleStatusIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import type { IModule } from "@plane/types"; // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control; diff --git a/web/components/modules/sidebar-select/select-status.tsx b/web/components/modules/sidebar-select/select-status.tsx index b8c337fd489..4a203ee626d 100644 --- a/web/components/modules/sidebar-select/select-status.tsx +++ b/web/components/modules/sidebar-select/select-status.tsx @@ -5,10 +5,10 @@ import { Control, Controller, UseFormWatch } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import { IModule } from "@plane/types"; // common // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control, any>; diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index ad3da373cd5..c9f28cf989a 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { @@ -14,13 +14,6 @@ import { Trash2, UserCircle2, } from "lucide-react"; -// hooks -import { useModule, useUser, useEventTracker } from "hooks/store"; -// components -import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; -import { DeleteModuleModal } from "components/modules"; -import ProgressChart from "components/core/sidebar/progress-chart"; -import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; // ui import { CustomMenu, @@ -32,15 +25,22 @@ import { TOAST_TYPE, setToast, } from "@plane/ui"; +// components +import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; +import ProgressChart from "components/core/sidebar/progress-chart"; +import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; +import { DeleteModuleModal } from "components/modules"; +// constant +import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; +import { MODULE_STATUS } from "constants/module"; +import { EUserProjectRoles } from "constants/project"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useModule, useUser, useEventTracker } from "hooks/store"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; -// constant -import { MODULE_STATUS } from "constants/module"; -import { EUserProjectRoles } from "constants/project"; -import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead_id: "", @@ -340,7 +340,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Date range
    -
    +
    = observer((props) => { control={control} name="lead_id" render={({ field: { value } }) => ( -
    +
    { @@ -408,7 +408,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="member_ids" render={({ field: { value } }) => ( -
    +
    { @@ -429,7 +429,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Issues
    -
    +
    {issueCount}
    diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index bd26dcfa588..03f75ca63b3 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -1,22 +1,22 @@ import React, { useEffect, useRef } from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; import { Menu } from "@headlessui/react"; -import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; -// hooks -import { useEventTracker } from "hooks/store"; // icons +import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; +// ui import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants +import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker"; import { snoozeOptions } from "constants/notification"; // helper -import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; +// hooks +import { useEventTracker } from "hooks/store"; // type import type { IUserNotification, NotificationType } from "@plane/types"; -// constants -import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker"; type NotificationCardProps = { selectedTab: NotificationType; @@ -215,7 +215,7 @@ export const NotificationCard: React.FC = (props) => { {notification.message}
    )} -
    +
    {({ open }) => ( <> @@ -231,11 +231,11 @@ export const NotificationCard: React.FC = (props) => {
    {moreOptions.map((item) => ( - + {({ close }) => (
    @@ -357,7 +358,7 @@ export const NotificationCard: React.FC = (props) => { }, }, ].map((item) => ( - +
diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index a8f25762eb8..d7aa1b07d8c 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -1,20 +1,20 @@ import React, { Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { Popover, Transition } from "@headlessui/react"; import { Bell } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; -import useUserNotification from "hooks/use-user-notifications"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components +import { Tooltip } from "@plane/ui"; import { EmptyState } from "components/common"; import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications"; -import { Tooltip } from "@plane/ui"; import { NotificationsLoader } from "components/ui"; +import { getNumberCount } from "helpers/string.helper"; +import { useApplication } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useUserNotification from "hooks/use-user-notifications"; +// components // images import emptyNotification from "public/empty-state/notification.svg"; // helpers -import { getNumberCount } from "helpers/string.helper"; export const NotificationPopover = observer(() => { // states diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index c2875b8dd4c..f65d51ba792 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -1,13 +1,13 @@ import { Fragment, FC } from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; -import { DateDropdown } from "components/dropdowns"; import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; +import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; +import { DateDropdown } from "components/dropdowns"; // constants import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"; // ui -import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -143,7 +143,7 @@ export const SnoozeNotificationModal: FC = (props) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
@@ -157,7 +157,7 @@ export const SnoozeNotificationModal: FC = (props) => {
-
+
Pick a date
= (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
@@ -221,10 +222,11 @@ export const SnoozeNotificationModal: FC = (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
diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index c176ed58025..2e94bb67ea3 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -1,23 +1,23 @@ import React, { useState } from "react"; import useSWR, { mutate } from "swr"; // hooks +import { CheckCircle2, Search } from "lucide-react"; +import { Button } from "@plane/ui"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; +import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; +import { ROLE } from "constants/workspace"; +import { truncateText } from "helpers/string.helper"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; // components -import { Button } from "@plane/ui"; // helpers -import { truncateText } from "helpers/string.helper"; // services import { WorkspaceService } from "services/workspace.service"; // constants -import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; -import { ROLE } from "constants/workspace"; -import { MEMBER_ACCEPTED } from "constants/event-tracker"; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // icons -import { CheckCircle2, Search } from "lucide-react"; import {} from "hooks/store/use-event-tracker"; -import { getUserRole } from "helpers/user.helper"; type Props = { handleNextStep: () => void; @@ -57,17 +57,18 @@ export const Invitations: React.FC = (props) => { }; const submitInvitations = async () => { - if (invitationsRespond.length <= 0) return; + const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); + + if (invitationsRespond.length <= 0 && !invitation?.role) return; setIsJoiningWorkspaces(true); - const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); await workspaceService .joinWorkspaces({ invitations: invitationsRespond }) .then(async () => { captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "SUCCESS", @@ -83,7 +84,7 @@ export const Invitations: React.FC = (props) => { console.error(error); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "FAILED", diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 1f78fcf204c..c5a0d51c23d 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; -import { Listbox, Transition } from "@headlessui/react"; import { Control, Controller, @@ -13,29 +12,30 @@ import { useFieldArray, useForm, } from "react-hook-form"; +import { Listbox, Transition } from "@headlessui/react"; +// icons import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; -// services -import { WorkspaceService } from "services/workspace.service"; -// hooks -import { useEventTracker } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { OnboardingStepIndicator } from "components/onboarding/step-indicator"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// types -import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; // helpers import { getUserRole } from "helpers/user.helper"; +// hooks +import { useEventTracker } from "hooks/store"; +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // assets -import user1 from "public/users/user-1.png"; -import user2 from "public/users/user-2.png"; import userDark from "public/onboarding/user-dark.svg"; import userLight from "public/onboarding/user-light.svg"; +import user1 from "public/users/user-1.png"; +import user2 from "public/users/user-2.png"; +// services +import { WorkspaceService } from "services/workspace.service"; +// types +import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; type Props = { finishOnboarding: () => Promise; @@ -368,8 +368,8 @@ export const InviteMembers: React.FC = (props) => { >

Members

- {Array.from({ length: 4 }).map(() => ( -
+ {Array.from({ length: 4 }).map((i) => ( +
user
diff --git a/web/components/onboarding/join-workspaces.tsx b/web/components/onboarding/join-workspaces.tsx index 08ffab379b0..e59db31c7e7 100644 --- a/web/components/onboarding/join-workspaces.tsx +++ b/web/components/onboarding/join-workspaces.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; import { useUser } from "hooks/store"; // components -import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; // types import { IWorkspace, TOnboardingSteps } from "@plane/types"; diff --git a/web/components/onboarding/onboarding-sidebar.tsx b/web/components/onboarding/onboarding-sidebar.tsx index af0da75ca93..42ec102cb3a 100644 --- a/web/components/onboarding/onboarding-sidebar.tsx +++ b/web/components/onboarding/onboarding-sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useTheme } from "next-themes"; import Image from "next/image"; +import { useTheme } from "next-themes"; import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { BarChart2, @@ -20,9 +20,9 @@ import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui"; // hooks import { useUser, useWorkspace } from "hooks/store"; // types +import projectEmoji from "public/emoji/project-emoji.svg"; import { IWorkspace } from "@plane/types"; // assets -import projectEmoji from "public/emoji/project-emoji.svg"; const workspaceLinks = [ { @@ -86,8 +86,9 @@ type Props = { watch?: UseFormWatch; userFullName?: string; }; -var timer: number = 0; -var lastWorkspaceName: string = ""; + +let timer: number = 0; +let lastWorkspaceName: string = ""; export const OnboardingSidebar: React.FC = (props) => { const { workspaceName, showProject, control, setValue, watch, userFullName } = props; diff --git a/web/components/onboarding/switch-delete-account-modal.tsx b/web/components/onboarding/switch-delete-account-modal.tsx index ff37e5802b4..c84911220c6 100644 --- a/web/components/onboarding/switch-delete-account-modal.tsx +++ b/web/components/onboarding/switch-delete-account-modal.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; type Props = { isOpen: boolean; diff --git a/web/components/onboarding/tour/root.tsx b/web/components/onboarding/tour/root.tsx index c09a2a94cf5..4c44f8c62a1 100644 --- a/web/components/onboarding/tour/root.tsx +++ b/web/components/onboarding/tour/root.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import { X } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; +import { TourSidebar } from "components/onboarding"; +import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // components -import { TourSidebar } from "components/onboarding"; // ui -import { Button } from "@plane/ui"; // assets -import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; -import IssuesTour from "public/onboarding/issues.webp"; import CyclesTour from "public/onboarding/cycles.webp"; +import IssuesTour from "public/onboarding/issues.webp"; import ModulesTour from "public/onboarding/modules.webp"; -import ViewsTour from "public/onboarding/views.webp"; import PagesTour from "public/onboarding/pages.webp"; +import ViewsTour from "public/onboarding/views.webp"; +import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; // constants -import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; type Props = { onComplete: () => void; diff --git a/web/components/onboarding/tour/sidebar.tsx b/web/components/onboarding/tour/sidebar.tsx index 350bd638a4f..53500249328 100644 --- a/web/components/onboarding/tour/sidebar.tsx +++ b/web/components/onboarding/tour/sidebar.tsx @@ -1,6 +1,6 @@ // icons -import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; import { FileText } from "lucide-react"; +import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; // types import { TTourSteps } from "./root"; diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index a29df3c9422..820f08da651 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -1,21 +1,22 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; import { Controller, useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { Camera, User2 } from "lucide-react"; -// hooks -import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -// components import { Button, Input } from "@plane/ui"; -import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; +// components import { UserImageUploadModal } from "components/core"; -// types -import { IUser } from "@plane/types"; -// services -import { FileService } from "services/file.service"; +import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; +// constants +import { USER_DETAILS } from "constants/event-tracker"; +// hooks +import { useEventTracker, useUser, useWorkspace } from "hooks/store"; // assets import IssuesSvg from "public/onboarding/onboarding-issues.webp"; -import { USER_DETAILS } from "constants/event-tracker"; +// services +import { FileService } from "services/file.service"; +// types +import { IUser } from "@plane/types"; const defaultValues: Partial = { first_name: "", @@ -183,7 +184,7 @@ export const UserDetails: React.FC = observer((props) => { name="first_name" type="text" value={value} - autoFocus={true} + autoFocus onChange={(event) => { setUserName(event.target.value); onChange(event); @@ -220,6 +221,7 @@ export const UserDetails: React.FC = observer((props) => {
{USE_CASES.map((useCase) => (
) => Promise; diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index 2f1d62f8488..7929a5e371b 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -2,13 +2,13 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignInRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components -import { SignInRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 0d5d4115a6a..2f8392bc2fd 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,20 +1,20 @@ import { useEffect } from "react"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { TourRoot } from "components/onboarding"; -import { UserGreetingsView } from "components/user"; -import { IssuePeekOverview } from "components/issues"; +import { Spinner } from "@plane/ui"; import { DashboardWidgets } from "components/dashboard"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { IssuePeekOverview } from "components/issues"; +import { TourRoot } from "components/onboarding"; +import { UserGreetingsView } from "components/user"; // ui -import { Spinner } from "@plane/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; export const WorkspaceDashboardView = observer(() => { // theme diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index eea7e9d7fdd..c3e22e52e93 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -2,15 +2,15 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // components -import { PageForm } from "./page-form"; -// hooks +import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; import { useEventTracker } from "hooks/store"; +// hooks // types -import { IPage } from "@plane/types"; import { useProjectPages } from "hooks/store/use-project-page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; +import { PageForm } from "./page-form"; // constants -import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; type Props = { // data?: IPage | null; diff --git a/web/components/pages/delete-page-modal.tsx b/web/components/pages/delete-page-modal.tsx index 67cd175f004..362dae172c6 100644 --- a/web/components/pages/delete-page-modal.tsx +++ b/web/components/pages/delete-page-modal.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, usePage } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { useProjectPages } from "hooks/store/use-project-page"; // constants import { PAGE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, usePage } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; +// types type TConfirmPageDeletionProps = { pageId: string; diff --git a/web/components/pages/page-form.tsx b/web/components/pages/page-form.tsx index 4f5874e5f5e..97e881096f7 100644 --- a/web/components/pages/page-form.tsx +++ b/web/components/pages/page-form.tsx @@ -2,10 +2,10 @@ import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, Tooltip } from "@plane/ui"; // types -import { IPage } from "@plane/types"; // constants import { PAGE_ACCESS_SPECIFIERS } from "constants/page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; type Props = { handleFormSubmit: (values: IPage) => Promise; diff --git a/web/components/pages/pages-list/all-pages-list.tsx b/web/components/pages/pages-list/all-pages-list.tsx index 4ed759a0f4d..e7cb2177567 100644 --- a/web/components/pages/pages-list/all-pages-list.tsx +++ b/web/components/pages/pages-list/all-pages-list.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const AllPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/archived-pages-list.tsx b/web/components/pages/pages-list/archived-pages-list.tsx index eb57d755803..f7bcb6059ba 100644 --- a/web/components/pages/pages-list/archived-pages-list.tsx +++ b/web/components/pages/pages-list/archived-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader, Spinner } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader, Spinner } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const ArchivedPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/favorite-pages-list.tsx b/web/components/pages/pages-list/favorite-pages-list.tsx index 4ce301a68f0..4d2ad5019b5 100644 --- a/web/components/pages/pages-list/favorite-pages-list.tsx +++ b/web/components/pages/pages-list/favorite-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const FavoritePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/list-item.tsx b/web/components/pages/pages-list/list-item.tsx index 6b1a4793d2c..d4cb3c02364 100644 --- a/web/components/pages/pages-list/list-item.tsx +++ b/web/components/pages/pages-list/list-item.tsx @@ -1,6 +1,7 @@ import { FC, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; import { AlertCircle, Archive, @@ -13,17 +14,16 @@ import { Star, Trash2, } from "lucide-react"; -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // components import { CreateUpdatePageModal, DeletePageModal } from "components/pages"; // constants import { EUserProjectRoles } from "constants/project"; -import { useRouter } from "next/router"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; +import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useMember, usePage, useUser } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; import { IIssueLabel } from "@plane/types"; export interface IPagesListItem { diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index ebd1fa12898..0d468ef3c2f 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -2,16 +2,16 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; // hooks +import { Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui -import { Loader } from "@plane/ui"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; diff --git a/web/components/pages/pages-list/private-page-list.tsx b/web/components/pages/pages-list/private-page-list.tsx index 15a577d80cd..316c6bf44bb 100644 --- a/web/components/pages/pages-list/private-page-list.tsx +++ b/web/components/pages/pages-list/private-page-list.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const PrivatePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 71bbf12ace2..28a4300312f 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -2,18 +2,18 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks +import { Loader } from "@plane/ui"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PagesListView } from "components/pages/pages-list"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { useApplication, useUser } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components -import { PagesListView } from "components/pages/pages-list"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Loader } from "@plane/ui"; // helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { // theme diff --git a/web/components/pages/pages-list/shared-pages-list.tsx b/web/components/pages/pages-list/shared-pages-list.tsx index d20a1350ef7..2626db13cd8 100644 --- a/web/components/pages/pages-list/shared-pages-list.tsx +++ b/web/components/pages/pages-list/shared-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const SharedPagesList: FC = observer(() => { diff --git a/web/components/profile/activity/activity-list.tsx b/web/components/profile/activity/activity-list.tsx index 06691272104..e77128e9f14 100644 --- a/web/components/profile/activity/activity-list.tsx +++ b/web/components/profile/activity/activity-list.tsx @@ -1,16 +1,16 @@ -import Link from "next/link"; +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; import { observer } from "mobx-react"; +import Link from "next/link"; import { History, MessageSquare } from "lucide-react"; // editor -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // hooks -import { useUser } from "hooks/store"; // components import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; // ui import { ActivitySettingsLoader } from "components/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; +import { useUser } from "hooks/store"; // types import { IUserActivityResponse } from "@plane/types"; diff --git a/web/components/profile/activity/download-button.tsx b/web/components/profile/activity/download-button.tsx index ff928dc2ad9..491ebf45fb8 100644 --- a/web/components/profile/activity/download-button.tsx +++ b/web/components/profile/activity/download-button.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { useRouter } from "next/router"; // services -import { UserService } from "services/user.service"; // ui import { Button } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { UserService } from "services/user.service"; const userService = new UserService(); diff --git a/web/components/profile/activity/profile-activity-list.tsx b/web/components/profile/activity/profile-activity-list.tsx index 3912c85680b..6311c382c56 100644 --- a/web/components/profile/activity/profile-activity-list.tsx +++ b/web/components/profile/activity/profile-activity-list.tsx @@ -1,22 +1,22 @@ import { useEffect } from "react"; -import Link from "next/link"; +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; import { observer } from "mobx-react"; +import Link from "next/link"; import useSWR from "swr"; import { History, MessageSquare } from "lucide-react"; // hooks +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +import { ActivitySettingsLoader } from "components/ui"; +import { USER_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; // editor -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // components -import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; // ui -import { ActivitySettingsLoader } from "components/ui"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; // fetch-keys -import { USER_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/activity/workspace-activity-list.tsx b/web/components/profile/activity/workspace-activity-list.tsx index c2c75a19564..aa5a03deec1 100644 --- a/web/components/profile/activity/workspace-activity-list.tsx +++ b/web/components/profile/activity/workspace-activity-list.tsx @@ -2,11 +2,11 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; import { UserService } from "services/user.service"; // components import { ActivityList } from "./activity-list"; // fetch-keys -import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 582f0f26bfb..ecc0028db9c 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // components import { ProfileIssuesFilter } from "components/profile"; diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 112c073abd2..4a6cf98bedb 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -1,21 +1,21 @@ +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; //hooks +import { Loader } from "@plane/ui"; +import { ActivityMessage, IssueLink } from "components/core"; +import { ProfileEmptyState } from "components/ui"; +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useUser } from "hooks/store"; // services +import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; import { UserService } from "services/user.service"; // components -import { ActivityMessage, IssueLink } from "components/core"; // ui -import { ProfileEmptyState } from "components/ui"; -import { Loader } from "@plane/ui"; // image -import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; // fetch-keys -import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); diff --git a/web/components/profile/overview/priority-distribution.tsx b/web/components/profile/overview/priority-distribution.tsx new file mode 100644 index 00000000000..12e430409c1 --- /dev/null +++ b/web/components/profile/overview/priority-distribution.tsx @@ -0,0 +1,88 @@ +// ui +import { Loader } from "@plane/ui"; +import { BarGraph, ProfileEmptyState } from "components/ui"; +// image +import { capitalizeFirstLetter } from "helpers/string.helper"; +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; +// helpers +// types +import { IUserProfileData } from "@plane/types"; + +type Props = { + userProfile: IUserProfileData | undefined; +}; + +export const ProfilePriorityDistribution: React.FC = ({ userProfile }) => ( +
+

Issues by Priority

+ {userProfile ? ( +
+ {userProfile.priority_distribution.length > 0 ? ( + ({ + priority: capitalizeFirstLetter(priority.priority ?? "None"), + value: priority.priority_count, + }))} + height="300px" + indexBy="priority" + keys={["value"]} + borderRadius={4} + padding={0.7} + customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)} + tooltip={(datum) => ( +
+ + {datum.data.priority}: + {datum.value} +
+ )} + colors={(datum) => { + if (datum.data.priority === "Urgent") return "#991b1b"; + else if (datum.data.priority === "High") return "#ef4444"; + else if (datum.data.priority === "Medium") return "#f59e0b"; + else if (datum.data.priority === "Low") return "#16a34a"; + else return "#e5e5e5"; + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + }, + grid: { + line: { + stroke: "transparent", + }, + }, + }} + /> + ) : ( +
+ +
+ )} +
+ ) : ( +
+ + + + + + + +
+ )} +
+); diff --git a/web/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/components/profile/overview/priority-distribution/priority-distribution.tsx index 63559bdeee6..48d612dffc5 100644 --- a/web/components/profile/overview/priority-distribution/priority-distribution.tsx +++ b/web/components/profile/overview/priority-distribution/priority-distribution.tsx @@ -1,9 +1,9 @@ // components -import { PriorityDistributionContent } from "./main-content"; // ui import { Loader } from "@plane/ui"; // types import { IUserPriorityDistribution } from "@plane/types"; +import { PriorityDistributionContent } from "./main-content"; type Props = { priorityDistribution: IUserPriorityDistribution[] | undefined; diff --git a/web/components/profile/overview/state-distribution.tsx b/web/components/profile/overview/state-distribution.tsx index f38283aa738..25de06c847d 100644 --- a/web/components/profile/overview/state-distribution.tsx +++ b/web/components/profile/overview/state-distribution.tsx @@ -1,11 +1,11 @@ // ui import { ProfileEmptyState, PieGraph } from "components/ui"; // image +import { STATE_GROUPS } from "constants/state"; import stateGraph from "public/empty-state/state_graph.svg"; // types import { IUserProfileData, IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; diff --git a/web/components/profile/overview/stats.tsx b/web/components/profile/overview/stats.tsx index 3f96488a0a7..62873ee3809 100644 --- a/web/components/profile/overview/stats.tsx +++ b/web/components/profile/overview/stats.tsx @@ -1,9 +1,9 @@ -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui -import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; import { UserCircle2 } from "lucide-react"; +import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; // types import { IUserProfileData } from "@plane/types"; diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index 86989748d7c..54e03b04708 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -1,7 +1,7 @@ // types +import { STATE_GROUPS } from "constants/state"; import { IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; diff --git a/web/components/profile/preferences/index.ts b/web/components/profile/preferences/index.ts index ddda5712c5c..56ef42216a4 100644 --- a/web/components/profile/preferences/index.ts +++ b/web/components/profile/preferences/index.ts @@ -1 +1 @@ -export * from "./email-notification-form"; \ No newline at end of file +export * from "./email-notification-form"; diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx index 5b4f9c8ed47..491c00f3acb 100644 --- a/web/components/profile/profile-issues-filter.tsx +++ b/web/components/profile/profile-issues-filter.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; // components import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues"; // hooks +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useIssues, useLabel } from "hooks/store"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ProfileIssuesFilter = observer(() => { diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 7e501764a4b..b6a99baf94e 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,20 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // components -import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; -import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; +import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // hooks +import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useIssues, useUser } from "hooks/store"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { EIssuesStoreType } from "constants/issue"; -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -41,8 +41,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { } = useIssues(EIssuesStoreType.PROFILE); useEffect(() => { - setViewId(type); - }, [type]); + if (setViewId) setViewId(type); + }, [type, setViewId]); useSWR( workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null, diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 48bb7d32382..e8b234cb893 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -1,25 +1,26 @@ import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; import useSWR from "swr"; +// ui import { Disclosure, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; +// icons +import { ChevronDown, Pencil } from "lucide-react"; +// plane ui +import { Loader, Tooltip } from "@plane/ui"; +// fetch-keys +import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { useApplication, useUser } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // services import { UserService } from "services/user.service"; // components import { ProfileSidebarTime } from "./time"; -// ui -import { Loader, Tooltip } from "@plane/ui"; -// icons -import { ChevronDown, Pencil } from "lucide-react"; -// helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// fetch-keys -import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; // services const userService = new UserService(); @@ -76,7 +77,7 @@ export const ProfileSidebar = observer(() => { return (
{userProjectsData ? ( @@ -106,7 +107,7 @@ export const ProfileSidebar = observer(() => { className="h-full w-full rounded object-cover" /> ) : ( -
+
{userProjectsData.user_data.first_name?.[0]}
)} diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 624e30bdef0..a19b53fbb3d 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // components -import { ProjectCard } from "components/project"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const ProjectCardList = observer(() => { // theme diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 9f554cfea8f..c74e9ee75e9 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -1,21 +1,20 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; import Link from "next/link"; -// hooks -import { useProject } from "hooks/store"; -// components -import { DeleteProjectModal, JoinProjectModal } from "components/project"; +import { useRouter } from "next/router"; +import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; +// components +import { DeleteProjectModal, JoinProjectModal, EUserProjectRoles } from "components/project"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; +// hooks +import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; export type ProjectCardProps = { project: IProject; diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index 7ab4afa0ae0..2c94c092d40 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import { IUserLite } from "@plane/types"; @@ -94,8 +94,8 @@ export const ConfirmProjectMemberRemove: React.FC = observer((props) => { ? "Leaving..." : "Leave" : isDeleteLoading - ? "Removing..." - : "Remove"} + ? "Removing..." + : "Remove"}
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index f7bbd92cf24..01cbb5888a0 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,23 +1,22 @@ import { useState, useEffect, Fragment, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; -// hooks -import { useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; -import EmojiIconPicker from "components/emoji-icon-picker"; import { MemberDropdown } from "components/dropdowns"; -// helpers -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +import EmojiIconPicker from "components/emoji-icon-picker"; // constants +import { PROJECT_CREATED } from "constants/event-tracker"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; -// constants import { EUserWorkspaceRoles } from "constants/workspace"; -import { PROJECT_CREATED } from "constants/event-tracker"; +// helpers +import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject, useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -306,7 +305,7 @@ export const CreateProjectModal: FC = observer((props) => { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} placeholder="Identifier" - className="w-full text-xs focus:border-blue-400 uppercase" + className="w-full text-xs uppercase focus:border-blue-400" tabIndex={2} /> )} diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 844bd3aadc4..bc26dcae453 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -4,13 +4,12 @@ import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks -import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -// ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { useEventTracker, useProject } from "hooks/store"; +// ui // types import type { IProject } from "@plane/types"; // constants -import { PROJECT_DELETED } from "constants/event-tracker"; type DeleteProjectModal = { isOpen: boolean; @@ -27,7 +26,6 @@ export const DeleteProjectModal: React.FC = (props) => { const { isOpen, project, onClose } = props; // store hooks const { captureProjectEvent } = useEventTracker(); - const { currentWorkspace } = useWorkspace(); const { deleteProject } = useProject(); // router const router = useRouter(); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index ef5a20024a0..25186e08e6c 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -1,23 +1,24 @@ import { FC, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -// hooks -import { useEventTracker, useProject } from "hooks/store"; -// components -import EmojiIconPicker from "components/emoji-icon-picker"; -import { ImagePickerPopover } from "components/core"; -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { Lock } from "lucide-react"; -// types -import { IProject, IWorkspace } from "@plane/types"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; +// ui +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; +import EmojiIconPicker from "components/emoji-icon-picker"; // constants +import { PROJECT_UPDATED } from "constants/event-tracker"; import { NETWORK_CHOICES } from "constants/project"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; // services import { ProjectService } from "services/project"; -import { PROJECT_UPDATED } from "constants/event-tracker"; +// types +import { IProject, IWorkspace } from "@plane/types"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index cf256098f1e..84be19f71c4 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -1,24 +1,17 @@ import React from "react"; - import Image from "next/image"; - -import useSWR, { mutate } from "swr"; - -// services -import { ProjectService } from "services/project"; -// hooks import { useRouter } from "next/router"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { SelectRepository, SelectChannel } from "components/integration"; +// constants +import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; // icons import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; -// ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspaceIntegration } from "@plane/types"; -// fetch-keys -import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; type Props = { integration: IWorkspaceIntegration; diff --git a/web/components/project/join-project-modal.tsx b/web/components/project/join-project-modal.tsx index 58b549b6c4b..384333581bb 100644 --- a/web/components/project/join-project-modal.tsx +++ b/web/components/project/join-project-modal.tsx @@ -2,9 +2,9 @@ import { useState, Fragment } from "react"; import { useRouter } from "next/router"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { Button } from "@plane/ui"; import { useProject, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import type { IProject } from "@plane/types"; diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 45618d4f246..6982d6316f5 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -1,17 +1,19 @@ import { FC, Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +// headless ui import { Dialog, Transition } from "@headlessui/react"; +// icons import { AlertTriangleIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useUser } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IProject } from "@plane/types"; // constants import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +// hooks +import { useEventTracker, useUser } from "hooks/store"; +// types +import { IProject } from "@plane/types"; type FormData = { projectName: string; diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 6bab775b8c0..43c2ce2a8b3 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -1,19 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; -// components -import { ConfirmProjectMemberRemove } from "components/project"; -// ui -import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import Link from "next/link"; +import { useRouter } from "next/router"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; +// ui +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmProjectMemberRemove } from "components/project"; // constants -import { ROLE } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +import { ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; type Props = { userId: string; diff --git a/web/components/project/member-list.tsx b/web/components/project/member-list.tsx index d7b43244530..7eaab01eff2 100644 --- a/web/components/project/member-list.tsx +++ b/web/components/project/member-list.tsx @@ -2,12 +2,12 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; // hooks -import { useEventTracker, useMember } from "hooks/store"; // components +import { Button } from "@plane/ui"; import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project"; // ui -import { Button } from "@plane/ui"; import { MembersSettingsLoader } from "components/ui"; +import { useEventTracker, useMember } from "hooks/store"; export const ProjectMemberList: React.FC = observer(() => { // states diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 7dc9b323325..491dbce0aa2 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -2,9 +2,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { Ban } from "lucide-react"; // hooks +import { Avatar, CustomSearchSelect } from "@plane/ui"; import { useMember } from "hooks/store"; // ui -import { Avatar, CustomSearchSelect } from "@plane/ui"; type Props = { value: any; diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index 91c06cdfcca..d6cffa7a44e 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -1,20 +1,19 @@ import { useEffect } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; -// hooks -import { useProject, useUser } from "hooks/store"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; - -import { MemberSelect } from "components/project"; +import useSWR from "swr"; // ui import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IProject, IUserLite, IWorkspace } from "@plane/types"; -// fetch-keys -import { PROJECT_MEMBERS } from "constants/fetch-keys"; +// components +import { MemberSelect } from "components/project"; // constants +import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { EUserProjectRoles } from "constants/project"; +// hooks +import { useProject, useUser } from "hooks/store"; +// types +import { IProject, IUserLite, IWorkspace } from "@plane/types"; const defaultValues: Partial = { project_lead: null, diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 64cf87fb5c2..048eb030694 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -1,17 +1,21 @@ import { Fragment, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +// ui import { Dialog, Transition } from "@headlessui/react"; +// icons import { Check, CircleDot, Globe2 } from "lucide-react"; -// hooks -import { useProjectPublish } from "hooks/store"; // ui import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; -import { CustomPopover } from "./popover"; +// hooks +import { useProjectPublish } from "hooks/store"; +// store +import { IProjectPublishSettings, TProjectPublishViews } from "store/project/project-publish.store"; // types import { IProject } from "@plane/types"; -import { IProjectPublishSettings, TProjectPublishViews } from "store/project/project-publish.store"; +// local components +import { CustomPopover } from "./popover"; type Props = { isOpen: boolean; @@ -359,16 +363,16 @@ export const PublishProjectModal: React.FC = observer((props) => { : "hover:bg-custom-background-80 hover:text-custom-text-100" }`} onClick={() => { - const _views = + const optionViews = value.length > 0 ? value.includes(option.key) ? value.filter((_o: string) => _o !== option.key) : [...value, option.key] : [option.key]; - if (_views.length === 0) return; + if (optionViews.length === 0) return; - onChange(_views); + onChange(optionViews); checkIfUpdateIsRequired(); }} > diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index da2f37e9f94..76b92ec995e 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -1,19 +1,15 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { ChevronDown, Plus, X } from "lucide-react"; // hooks -import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; // ui import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { getUserRole } from "helpers/user.helper"; +import { useEventTracker, useMember, useUser } from "hooks/store"; // constants -import { ROLE } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; -import { PROJECT_MEMBER_ADDED } from "constants/event-tracker"; type Props = { isOpen: boolean; diff --git a/web/components/project/settings/delete-project-section.tsx b/web/components/project/settings/delete-project-section.tsx index fa1d70d5c35..0f0e41fa7db 100644 --- a/web/components/project/settings/delete-project-section.tsx +++ b/web/components/project/settings/delete-project-section.tsx @@ -2,9 +2,9 @@ import React from "react"; // ui import { Disclosure, Transition } from "@headlessui/react"; +import { ChevronDown, ChevronUp } from "lucide-react"; import { Button, Loader } from "@plane/ui"; // icons -import { ChevronDown, ChevronUp } from "lucide-react"; // types import { IProject } from "@plane/types"; diff --git a/web/components/project/settings/features-list.tsx b/web/components/project/settings/features-list.tsx index efbcc085765..188d103ba95 100644 --- a/web/components/project/settings/features-list.tsx +++ b/web/components/project/settings/features-list.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ContrastIcon, FileText, Inbox, Layers } from "lucide-react"; -// hooks -import { useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; // ui -import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IProject } from "@plane/types"; +import { DiceIcon, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; - -type Props = {}; +// hooks +import { useEventTracker, useProject, useUser } from "hooks/store"; +// types +import { IProject } from "@plane/types"; const PROJECT_FEATURES_LIST = [ { @@ -46,7 +44,7 @@ const PROJECT_FEATURES_LIST = [ }, ]; -export const ProjectFeaturesList: FC = observer(() => { +export const ProjectFeaturesList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 00dc858d0c5..c86ba0dc22b 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -1,9 +1,9 @@ import { useRef, useState } from "react"; +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { Disclosure, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // icons import { MoreVertical, @@ -18,13 +18,6 @@ import { MoreHorizontal, Inbox, } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// helpers -import { cn } from "helpers/common.helper"; -import { getNumberCount } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // ui import { CustomMenu, @@ -36,9 +29,17 @@ import { LayersIcon, setPromiseToast, } from "@plane/ui"; -// components import { LeaveProjectModal, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +import { getNumberCount } from "helpers/string.helper"; +// hooks +import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// helpers + +// components type Props = { projectId: string; diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 05e09f565de..2cee91e6bd3 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -1,21 +1,21 @@ -import { useState, FC, useRef, useEffect, useCallback } from "react"; -import { useRouter } from "next/router"; +import { useState, FC, useRef, useEffect } from "react"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; -import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { cn } from "helpers/common.helper"; +import { orderJoinedProjects } from "helpers/project.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { orderJoinedProjects } from "helpers/project.helper"; -import { cn } from "helpers/common.helper"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { IProject } from "@plane/types"; export const ProjectSidebarList: FC = observer(() => { @@ -63,8 +63,8 @@ export const ProjectSidebarList: FC = observer(() => { const joinedProjectsList: IProject[] = []; joinedProjects.map((projectId) => { - const _project = getProjectById(projectId); - if (_project) joinedProjectsList.push(_project); + const projectDetails = getProjectById(projectId); + if (projectDetails) joinedProjectsList.push(projectDetails); }); if (joinedProjectsList.length <= 0) return; diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index f39e3f33585..b142cc60e58 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -1,19 +1,19 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; import { Dialog, Popover, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useProjectState } from "hooks/store"; -// ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { ChevronDown } from "lucide-react"; -// types -import type { IState } from "@plane/types"; +// ui +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { GROUP_CHOICES } from "constants/project"; +// hooks +import { useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; // types type Props = { diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index 0a50208cd1f..88c50a01737 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -1,18 +1,18 @@ import React, { useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useForm, Controller } from "react-hook-form"; import { TwitterPicker } from "react-color"; +import { useForm, Controller } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui import { Button, CustomSelect, Input, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import type { IState } from "@plane/types"; // constants -import { GROUP_CHOICES } from "constants/project"; import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker"; +import { GROUP_CHOICES } from "constants/project"; +// hooks +import { useEventTracker, useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; type Props = { data: IState | null; diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index df47c8b12ef..7496b53b44b 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import type { IState } from "@plane/types"; // constants import { STATE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/states/project-setting-state-list-item.tsx b/web/components/states/project-setting-state-list-item.tsx index 401c482f3ab..760c8501ccb 100644 --- a/web/components/states/project-setting-state-list-item.tsx +++ b/web/components/states/project-setting-state-list-item.tsx @@ -1,14 +1,14 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui +import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; import { Tooltip, StateGroupIcon } from "@plane/ui"; // icons -import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { useEventTracker, useProjectState } from "hooks/store"; // types import { IState } from "@plane/types"; diff --git a/web/components/states/project-setting-state-list.tsx b/web/components/states/project-setting-state-list.tsx index 99ac40d84f0..5f77725670e 100644 --- a/web/components/states/project-setting-state-list.tsx +++ b/web/components/states/project-setting-state-list.tsx @@ -1,20 +1,20 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { Plus } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; +import { STATES_LIST } from "constants/fetch-keys"; +import { sortByField } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; import { useEventTracker, useProjectState } from "hooks/store"; // components -import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; // ui -import { Loader } from "@plane/ui"; // icons -import { Plus } from "lucide-react"; // helpers -import { orderStateGroups } from "helpers/state.helper"; -import { sortByField } from "helpers/array.helper"; // fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; export const ProjectSettingStateList: React.FC = observer(() => { // router diff --git a/web/components/toast-alert/index.tsx b/web/components/toast-alert/index.tsx new file mode 100644 index 00000000000..80594e21825 --- /dev/null +++ b/web/components/toast-alert/index.tsx @@ -0,0 +1,61 @@ +import React from "react"; +// hooks +import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react"; +import useToast from "hooks/use-toast"; +// icons + +const ToastAlerts = () => { + const { alerts, removeAlert } = useToast(); + + if (!alerts) return null; + + return ( +
+ {alerts.map((alert) => ( +
+
+ +
+
+
+
+ {alert.type === "success" ? ( +
+
+

{alert.title}

+ {alert.message &&

{alert.message}

} +
+
+
+
+ ))} +
+ ); +}; + +export default ToastAlerts; diff --git a/web/components/ui/empty-space.tsx b/web/components/ui/empty-space.tsx index 4b70bbb158d..73fc6ba01a6 100644 --- a/web/components/ui/empty-space.tsx +++ b/web/components/ui/empty-space.tsx @@ -1,7 +1,7 @@ // next +import React from "react"; import Link from "next/link"; // react -import React from "react"; // icons import { ChevronRight } from "lucide-react"; diff --git a/web/components/ui/graphs/bar-graph.tsx b/web/components/ui/graphs/bar-graph.tsx index 3756b0455e0..3f40aad8701 100644 --- a/web/components/ui/graphs/bar-graph.tsx +++ b/web/components/ui/graphs/bar-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveBar, BarSvgProps } from "@nivo/bar"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { indexBy: string; diff --git a/web/components/ui/graphs/calendar-graph.tsx b/web/components/ui/graphs/calendar-graph.tsx index 0725c425afb..a64a4a92058 100644 --- a/web/components/ui/graphs/calendar-graph.tsx +++ b/web/components/ui/graphs/calendar-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveCalendar, CalendarSvgProps } from "@nivo/calendar"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const CalendarGraph: React.FC> = ({ height = "400px", diff --git a/web/components/ui/graphs/line-graph.tsx b/web/components/ui/graphs/line-graph.tsx index 91a19acc318..93eac009721 100644 --- a/web/components/ui/graphs/line-graph.tsx +++ b/web/components/ui/graphs/line-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveLine, LineSvgProps } from "@nivo/line"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { customYAxisTickValues?: number[]; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx new file mode 100644 index 00000000000..c0e6eb300e8 --- /dev/null +++ b/web/components/ui/graphs/marimekko-graph.tsx @@ -0,0 +1,48 @@ +// nivo +import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; +// helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; +import { generateYAxisTickValues } from "helpers/graph.helper"; +// types +import { TGraph } from "./types"; +// constants + +type Props = { + id: string; + value: string; + customYAxisTickValues?: number[]; +}; + +export const MarimekkoGraph: React.FC, "height" | "width">> = ({ + id, + value, + customYAxisTickValues, + height = "400px", + width = "100%", + margin, + theme, + ...rest +}) => ( +
+ 7 ? -45 : 0, + }} + labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} + theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} + animate + {...rest} + /> +
+); diff --git a/web/components/ui/graphs/pie-graph.tsx b/web/components/ui/graphs/pie-graph.tsx index 52b56e4926a..739ede4b0aa 100644 --- a/web/components/ui/graphs/pie-graph.tsx +++ b/web/components/ui/graphs/pie-graph.tsx @@ -1,9 +1,9 @@ // nivo import { PieSvgProps, ResponsivePie } from "@nivo/pie"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const PieGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/graphs/scatter-plot-graph.tsx b/web/components/ui/graphs/scatter-plot-graph.tsx index c6ff5a77228..4eb82a97efa 100644 --- a/web/components/ui/graphs/scatter-plot-graph.tsx +++ b/web/components/ui/graphs/scatter-plot-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const ScatterPlotGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/loader/cycle-module-board-loader.tsx b/web/components/ui/loader/cycle-module-board-loader.tsx index 09c885fb928..f88719c381a 100644 --- a/web/components/ui/loader/cycle-module-board-loader.tsx +++ b/web/components/ui/loader/cycle-module-board-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleBoardLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/cycle-module-list-loader.tsx b/web/components/ui/loader/cycle-module-list-loader.tsx index 8787a142541..522b96f0da7 100644 --- a/web/components/ui/loader/cycle-module-list-loader.tsx +++ b/web/components/ui/loader/cycle-module-list-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleListLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx index 944ec02b874..3456e43ab54 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx @@ -1,7 +1,7 @@ import React from "react"; // ui -import { InboxSidebarLoader } from "./inbox-sidebar-loader"; import { Loader } from "@plane/ui"; +import { InboxSidebarLoader } from "./inbox-sidebar-loader"; export const InboxLayoutLoader = () => (
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx index ce464e83d92..204c2fff600 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx @@ -7,8 +7,8 @@ export const InboxSidebarLoader = () => (
- {[...Array(6)].map(() => ( -
+ {[...Array(6)].map((i) => ( +
diff --git a/web/components/ui/loader/notification-loader.tsx b/web/components/ui/loader/notification-loader.tsx index 143f1a9b64a..7485c2c4c39 100644 --- a/web/components/ui/loader/notification-loader.tsx +++ b/web/components/ui/loader/notification-loader.tsx @@ -1,7 +1,7 @@ export const NotificationsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/pages-loader.tsx b/web/components/ui/loader/pages-loader.tsx index e31e8294216..612c17d888a 100644 --- a/web/components/ui/loader/pages-loader.tsx +++ b/web/components/ui/loader/pages-loader.tsx @@ -4,13 +4,13 @@ export const PagesLoader = () => (

Pages

- {[...Array(5)].map(() => ( - + {[...Array(5)].map((i) => ( + ))}
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/projects-loader.tsx b/web/components/ui/loader/projects-loader.tsx index 9548a1f483b..d1a781d6b71 100644 --- a/web/components/ui/loader/projects-loader.tsx +++ b/web/components/ui/loader/projects-loader.tsx @@ -1,8 +1,11 @@ export const ProjectsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/activity.tsx b/web/components/ui/loader/settings/activity.tsx index 7bc5c392f4d..70297f64486 100644 --- a/web/components/ui/loader/settings/activity.tsx +++ b/web/components/ui/loader/settings/activity.tsx @@ -2,8 +2,8 @@ import { getRandomLength } from "../utils"; export const ActivitySettingsLoader = () => (
- {[...Array(10)].map(() => ( -
+ {[...Array(10)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/api-token.tsx b/web/components/ui/loader/settings/api-token.tsx index fc5b4c41d60..e31090bffd9 100644 --- a/web/components/ui/loader/settings/api-token.tsx +++ b/web/components/ui/loader/settings/api-token.tsx @@ -5,8 +5,8 @@ export const APITokenSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/email.tsx b/web/components/ui/loader/settings/email.tsx index fa68b972f5a..87634bf090a 100644 --- a/web/components/ui/loader/settings/email.tsx +++ b/web/components/ui/loader/settings/email.tsx @@ -8,8 +8,8 @@ export const EmailSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/import-and-export.tsx b/web/components/ui/loader/settings/import-and-export.tsx index 70496d1c19a..a3561207d11 100644 --- a/web/components/ui/loader/settings/import-and-export.tsx +++ b/web/components/ui/loader/settings/import-and-export.tsx @@ -1,7 +1,7 @@ export const ImportExportSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/integration.tsx b/web/components/ui/loader/settings/integration.tsx index 871b570b19b..2260517ee39 100644 --- a/web/components/ui/loader/settings/integration.tsx +++ b/web/components/ui/loader/settings/integration.tsx @@ -1,7 +1,10 @@ export const IntegrationsSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/members.tsx b/web/components/ui/loader/settings/members.tsx index 3ed2c41efc3..e286320a90a 100644 --- a/web/components/ui/loader/settings/members.tsx +++ b/web/components/ui/loader/settings/members.tsx @@ -1,7 +1,7 @@ export const MembersSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/view-list-loader.tsx b/web/components/ui/loader/view-list-loader.tsx index 97899a65764..8b59b57a264 100644 --- a/web/components/ui/loader/view-list-loader.tsx +++ b/web/components/ui/loader/view-list-loader.tsx @@ -1,7 +1,7 @@ export const ViewListLoader = () => (
- {[...Array(8)].map(() => ( -
+ {[...Array(8)].map((i) => ( +
diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 7bf4aa8a126..8bb0ebcf3a0 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -3,9 +3,9 @@ import { Fragment, useState } from "react"; // headless ui import { Menu, Transition } from "@headlessui/react"; // ui +import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; import { Loader } from "@plane/ui"; // icons -import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; type MultiLevelDropdownProps = { label: string; @@ -71,10 +71,10 @@ export const MultiLevelDropdown: React.FC = ({
{ + onClick={(e: unknown) => { if (option.hasChildren) { - e.stopPropagation(); - e.preventDefault(); + e?.stopPropagation(); + e?.preventDefault(); if (option.onClick) option.onClick(); diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index f4fe8e12051..180c293e09b 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useProjectView } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useProjectView } from "hooks/store"; // types import { IProjectView } from "@plane/types"; diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 0da7e394684..31fee10064d 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -2,15 +2,15 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, TextArea } from "@plane/ui"; +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useLabel, useMember, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // types import { IProjectView, IIssueFilterOptions } from "@plane/types"; // constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; type Props = { data?: IProjectView | null; @@ -212,8 +212,8 @@ export const ProjectViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index a1abef1a4a0..7e0c92f268a 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -1,12 +1,12 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useProjectView } from "hooks/store"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ProjectViewForm } from "components/views"; +// hooks +import { useProjectView } from "hooks/store"; // types import { IProjectView } from "@plane/types"; diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 7ff1ee92e48..29d5bac57cc 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -1,21 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; -// hooks -import { useProjectView, useUser } from "hooks/store"; -// components -import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; +// constants +import { EUserProjectRoles } from "constants/project"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useProjectView, useUser } from "hooks/store"; // types import { IProjectView } from "@plane/types"; -// constants -import { EUserProjectRoles } from "constants/project"; type Props = { view: IProjectView; diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 4977ed3e7c3..9d8bf85e6f7 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,18 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { Search } from "lucide-react"; import { useTheme } from "next-themes"; +import { Search } from "lucide-react"; // hooks -import { useApplication, useProjectView, useUser } from "hooks/store"; // components -import { ProjectViewListItem } from "components/views"; +import { Input } from "@plane/ui"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Input } from "@plane/ui"; import { ViewListLoader } from "components/ui"; +import { ProjectViewListItem } from "components/views"; // constants -import { EUserProjectRoles } from "constants/project"; import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useProjectView, useUser } from "hooks/store"; export const ProjectViewsList = observer(() => { // states diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx index ecbd4ccd3ff..b18beede897 100644 --- a/web/components/web-hooks/create-webhook-modal.tsx +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +// ui import { Dialog, Transition } from "@headlessui/react"; +import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { WebhookForm } from "./form"; -import { GeneratedHookDetails } from "./generated-hook-details"; -// hooks // helpers import { csvDownload } from "helpers/download.helper"; +// types +import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; +import { WebhookForm } from "./form"; +import { GeneratedHookDetails } from "./generated-hook-details"; // utils import { getCurrentHookAsCSV } from "./utils"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; -// types -import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; interface ICreateWebhookModal { currentWorkspace: IWorkspace | null; diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx index 52c7a6595e8..22f8aca32b9 100644 --- a/web/components/web-hooks/delete-webhook-modal.tsx +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -2,10 +2,10 @@ import React, { FC, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useWebhook } from "hooks/store"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useWebhook } from "hooks/store"; interface IDeleteWebhook { isOpen: boolean; diff --git a/web/components/web-hooks/form/form.tsx b/web/components/web-hooks/form/form.tsx index c2dd940dc2e..1b1e1bf2760 100644 --- a/web/components/web-hooks/form/form.tsx +++ b/web/components/web-hooks/form/form.tsx @@ -1,9 +1,8 @@ import React, { FC, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks -import { useWebhook } from "hooks/store"; -// components +import { Button } from "@plane/ui"; import { WebhookIndividualEventOptions, WebhookInput, @@ -11,8 +10,9 @@ import { WebhookSecretKey, WebhookToggle, } from "components/web-hooks"; +import { useWebhook } from "hooks/store"; +// components // ui -import { Button } from "@plane/ui"; // types import { IWebhook, TWebhookEventTypes } from "@plane/types"; @@ -36,7 +36,7 @@ export const WebhookForm: FC = observer((props) => { // states const [webhookEventType, setWebhookEventType] = useState("all"); // store hooks - const {webhookSecretKey } = useWebhook(); + const { webhookSecretKey } = useWebhook(); // use form const { handleSubmit, diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 7e9d9deda22..11129fb071d 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -1,18 +1,19 @@ import { useState, FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; +// icons import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useWebhook, useWorkspace } from "hooks/store"; -// helpers -import { copyTextToClipboard } from "helpers/string.helper"; -import { csvDownload } from "helpers/download.helper"; -// utils -import { getCurrentHookAsCSV } from "../utils"; // ui import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { csvDownload } from "helpers/download.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; +// hooks +import { useWebhook, useWorkspace } from "hooks/store"; // types import { IWebhook } from "@plane/types"; +// utils +import { getCurrentHookAsCSV } from "../utils"; type Props = { data: Partial; diff --git a/web/components/web-hooks/generated-hook-details.tsx b/web/components/web-hooks/generated-hook-details.tsx index ce78fa5d5da..2cd5ef98692 100644 --- a/web/components/web-hooks/generated-hook-details.tsx +++ b/web/components/web-hooks/generated-hook-details.tsx @@ -1,9 +1,9 @@ // components -import { WebhookSecretKey } from "./form"; // ui import { Button } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; +import { WebhookSecretKey } from "./form"; type Props = { handleClose: () => void; diff --git a/web/components/web-hooks/webhooks-list-item.tsx b/web/components/web-hooks/webhooks-list-item.tsx index fa676fccdcd..2f9ca52a543 100644 --- a/web/components/web-hooks/webhooks-list-item.tsx +++ b/web/components/web-hooks/webhooks-list-item.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks +import { ToggleSwitch } from "@plane/ui"; import { useWebhook } from "hooks/store"; // ui -import { ToggleSwitch } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; diff --git a/web/components/workspace/confirm-workspace-member-remove.tsx b/web/components/workspace/confirm-workspace-member-remove.tsx index 6c5ec4593ca..a11938472b2 100644 --- a/web/components/workspace/confirm-workspace-member-remove.tsx +++ b/web/components/workspace/confirm-workspace-member-remove.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; type Props = { isOpen: boolean; @@ -102,8 +102,8 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => ? "Leaving" : "Leave" : isRemoving - ? "Removing" - : "Remove"} + ? "Removing" + : "Remove"}
diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index e8e40cf8536..822ee1347f5 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -1,18 +1,17 @@ import { Dispatch, SetStateAction, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; -// services -import { WorkspaceService } from "services/workspace.service"; +// ui +import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { WORKSPACE_CREATED } from "constants/event-tracker"; +import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; // ui -import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; -// constants -import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; -import { WORKSPACE_CREATED } from "constants/event-tracker"; type Props = { onSubmit?: (res: IWorkspace) => Promise; @@ -21,7 +20,7 @@ type Props = { slug: string; organization_size: string; }; - setDefaultValues: Dispatch>; + setDefaultValues: Dispatch>; secondaryButton?: React.ReactNode; primaryButtonText?: { loading: string; diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index dbb2ef4f026..0691fbbf072 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -1,17 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useWorkspace } from "hooks/store"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -// types -import type { IWorkspace } from "@plane/types"; // constants import { WORKSPACE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useWorkspace } from "hooks/store"; +// types +import type { IWorkspace } from "@plane/types"; type Props = { isOpen: boolean; @@ -55,7 +55,7 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { if (!data || !canDelete) return; await deleteWorkspace(data.slug) - .then((res) => { + .then(() => { handleClose(); router.push("/"); captureWorkspaceEvent({ diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index 0bb77f9c74c..210bbbd3aa6 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -1,17 +1,19 @@ import React, { useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; +// headless ui import { Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useApplication } from "hooks/store"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; +// ui import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; +// hooks +import { useApplication } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // assets import packageJson from "package.json"; -const helpOptions = [ +const HELP_OPTIONS = [ { name: "Documentation", href: "https://docs.plane.so/", @@ -27,12 +29,6 @@ const helpOptions = [ href: "https://github.com/makeplane/plane/issues/new/choose", Icon: GithubIcon, }, - { - name: "Chat with us", - href: null, - onClick: () => (window as any).$crisp.push(["do", "chat:show"]), - Icon: MessagesSquare, - }, ]; export interface WorkspaceHelpSectionProps { @@ -45,12 +41,17 @@ export const WorkspaceHelpSection: React.FC = observe theme: { sidebarCollapsed, toggleSidebar }, commandPalette: { toggleShortcutModal }, } = useApplication(); - // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs const helpOptionsRef = useRef(null); + const handleCrispWindowShow = () => { + if (window) { + window.$crisp.push(["do", "chat:show"]); + } + }; + useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false)); const isCollapsed = sidebarCollapsed || false; @@ -129,33 +130,26 @@ export const WorkspaceHelpSection: React.FC = observe ref={helpOptionsRef} >
- {helpOptions.map(({ name, Icon, href, onClick }) => { - if (href) - return ( - - -
- -
- {name} -
- - ); - else - return ( - - ); - })} + {HELP_OPTIONS.map(({ name, Icon, href }) => ( + + +
+ +
+ {name} +
+ + ))} +
Version: v{packageJson.version}
diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 25f4c3c7298..55f64bfab90 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { Plus, X } from "lucide-react"; -// hooks -import { useUser } from "hooks/store"; // ui import { Button, CustomSelect, Input } from "@plane/ui"; -// types -import { IWorkspaceBulkInviteFormData } from "@plane/types"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useUser } from "hooks/store"; +// types +import { IWorkspaceBulkInviteFormData } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/workspace/settings/invitations-list-item.tsx b/web/components/workspace/settings/invitations-list-item.tsx index 9a9df5cb1f5..8c6de24b25a 100644 --- a/web/components/workspace/settings/invitations-list-item.tsx +++ b/web/components/workspace/settings/invitations-list-item.tsx @@ -1,15 +1,15 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ChevronDown, XCircle } from "lucide-react"; -// hooks -import { useMember, useUser } from "hooks/store"; -// components -import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useMember, useUser } from "hooks/store"; type Props = { invitationId: string; diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index c6c8d1d364e..f40d78bb0f8 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -1,17 +1,18 @@ import { useState, FC } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +// lucide icons import { ChevronDown, Dot, XCircle } from "lucide-react"; -// hooks -import { useEventTracker, useMember, useUser } from "hooks/store"; -// components -import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // ui import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmWorkspaceMemberRemove } from "components/workspace"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useUser } from "hooks/store"; type Props = { memberId: string; diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index 1dc02d508e3..216122525b9 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -1,13 +1,12 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; -// hooks -import { useMember } from "hooks/store"; // components -import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; -// ui import { MembersSettingsLoader } from "components/ui"; +import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; +// hooks +import { useMember } from "hooks/store"; export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => { const { searchQuery } = props; diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index d491ca08e45..bfd1473ea06 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -3,22 +3,22 @@ import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; -// services -import { FileService } from "services/file.service"; -// hooks -import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -// components -import { DeleteWorkspaceModal } from "components/workspace"; -import { WorkspaceImageUploadModal } from "components/core"; // ui import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { WorkspaceImageUploadModal } from "components/core"; +import { DeleteWorkspaceModal } from "components/workspace"; +// constants +import { WORKSPACE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useUser, useWorkspace } from "hooks/store"; +// services +import { FileService } from "services/file.service"; // types import { IWorkspace } from "@plane/types"; -// constants -import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; -import { WORKSPACE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { name: "", diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 98a133ee356..5d1695b3368 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -1,16 +1,18 @@ import { Fragment, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -import { Menu, Transition } from "@headlessui/react"; +import { usePopper } from "react-popper"; import { mutate } from "swr"; +// ui +import { Menu, Transition } from "@headlessui/react"; +// icons import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; -import { usePopper } from "react-popper"; +// plane ui +import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -// ui -import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; // Static Data diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 774a231db75..2069d8f27ef 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -1,20 +1,20 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -// hooks -import { useApplication, useEventTracker, useUser } from "hooks/store"; -// components -import { NotificationPopover } from "components/notifications"; +import { Crown } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; -import { Crown } from "lucide-react"; +// components +import { NotificationPopover } from "components/notifications"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { SIDEBAR_MENU_ITEMS } from "constants/dashboard"; import { SIDEBAR_CLICKED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useApplication, useEventTracker, useUser } from "hooks/store"; export const WorkspaceSidebarMenu = observer(() => { // store hooks diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index dd2dd5c6878..d2ce2f5b34b 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -1,14 +1,14 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { ChevronUp, PenSquare, Search } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // components import { CreateUpdateIssueModal } from "components/issues"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { EIssuesStoreType } from "constants/issue"; +import { EUserWorkspaceRoles } from "constants/workspace"; +// hooks +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // types import { TIssue } from "@plane/types"; @@ -27,11 +27,12 @@ export const WorkspaceSidebarQuickAction = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); - const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); + const { storedValue } = useLocalStorage>>("draftedIssue", {}); //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const timeoutRef = useRef(); const isSidebarCollapsed = themeStore.sidebarCollapsed; @@ -41,7 +42,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { const disabled = joinedProjectIds.length === 0; const onMouseEnter = () => { - //if renet before timout clear the timeout + // if enter before time out clear the timeout timeoutRef?.current && clearTimeout(timeoutRef.current); setIsDraftButtonOpen(true); }; @@ -68,7 +69,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { onClose={() => setIsDraftIssueModalOpen(false)} data={workspaceDraftIssue ?? {}} onSubmit={() => removeWorkspaceDraftIssue()} - isDraft={true} + isDraft />
) => Promise; @@ -200,8 +200,8 @@ export const WorkspaceViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index 223fda13c11..97982e61e0b 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,15 +1,16 @@ import React, { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Plus } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView, useUser } from "hooks/store"; // components import { CreateUpdateWorkspaceViewModal } from "components/workspace"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; import { GLOBAL_VIEW_OPENED } from "constants/event-tracker"; +import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; +// store hooks +import { useEventTracker, useGlobalView, useUser } from "hooks/store"; const ViewTab = observer((props: { viewId: string }) => { const { viewId } = props; @@ -69,7 +70,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { activeTabElement.scrollIntoView({ behavior: "smooth", inline: diff > 500 ? "center" : "nearest" }); } } - }, [globalViewId, currentWorkspaceViews, containerRef]); + }, [globalViewId, currentWorkspaceViews, containerRef, captureEvent]); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -95,9 +96,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { ))} - {currentWorkspaceViews?.map((viewId) => ( - - ))} + {currentWorkspaceViews?.map((viewId) => )}
{isAuthorizedUser && ( diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index 6543a83212b..975018f1643 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -1,17 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { WorkspaceViewForm } from "components/workspace"; -// types -import { IWorkspaceView } from "@plane/types"; // constants import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; +// types +import { IWorkspaceView } from "@plane/types"; type Props = { data?: IWorkspaceView; diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index 28f25551cd6..4030dc1819a 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -1,17 +1,18 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Pencil, Trash2 } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; -// components -import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // ui import { CustomMenu } from "@plane/ui"; +// components +import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // helpers -import { truncateText } from "helpers/string.helper"; import { calculateTotalFilters } from "helpers/filter.helper"; +import { truncateText } from "helpers/string.helper"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; type Props = { viewId: string }; diff --git a/web/components/workspace/views/views-list.tsx b/web/components/workspace/views/views-list.tsx index 9a8758d2dc9..ef33fe16eb7 100644 --- a/web/components/workspace/views/views-list.tsx +++ b/web/components/workspace/views/views-list.tsx @@ -1,12 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; -// store hooks -import { useGlobalView } from "hooks/store"; // components -import { GlobalViewListItem } from "components/workspace"; -// ui import { ViewListLoader } from "components/ui"; +import { GlobalViewListItem } from "components/workspace"; +// store hooks +import { useGlobalView } from "hooks/store"; type Props = { searchQuery: string; @@ -29,11 +28,5 @@ export const GlobalViewsList: React.FC = observer((props) => { const filteredViewsList = getSearchedViews(searchQuery); - return ( - <> - {filteredViewsList?.map((viewId) => ( - - ))} - - ); + return <>{filteredViewsList?.map((viewId) => )}; }); diff --git a/web/components/workspace/workspace-active-cycles-upgrade.tsx b/web/components/workspace/workspace-active-cycles-upgrade.tsx index b5a61610b4d..23ab27acf35 100644 --- a/web/components/workspace/workspace-active-cycles-upgrade.tsx +++ b/web/components/workspace/workspace-active-cycles-upgrade.tsx @@ -1,16 +1,16 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react"; -// hooks -import { useUser } from "hooks/store"; -// ui -import { getButtonStyling } from "@plane/ui"; +import Image from "next/image"; // icons import { Crown } from "lucide-react"; -// helper -import { cn } from "helpers/common.helper"; +// ui +import { getButtonStyling } from "@plane/ui"; // constants import { WORKSPACE_ACTIVE_CYCLES_DETAILS } from "constants/cycle"; +// helper +import { cn } from "helpers/common.helper"; +// hooks +import { useUser } from "hooks/store"; export const WorkspaceActiveCyclesUpgrade = observer(() => { // store hooks @@ -75,7 +75,7 @@ export const WorkspaceActiveCyclesUpgrade = observer(() => {
{WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => ( -
+

{item.title}

diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 63900b6b76e..8bb43d898b6 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -162,5 +162,3 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [ icon: Microscope, }, ]; - - diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 6ac4e78174c..a3f5f7e0028 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -1,19 +1,20 @@ import { linearGradientDef } from "@nivo/core"; // assets -import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; -import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; -import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; -import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; +import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; +import { ContrastIcon } from "@plane/ui"; +import { Props } from "components/icons/types"; import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; +import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; +import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; +import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; +import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; // icons -import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; -import { ContrastIcon } from "@plane/ui"; // gradients for issues by priority widget graph bars export const PRIORITY_GRAPH_GRADIENTS = [ diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 37a18a37db6..7edfccba5f4 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -112,16 +112,16 @@ export const getIssueEventPayload = (props: IssueEventProps) => { updated_from: props.path?.includes("workspace-views") ? "All views" : props.path?.includes("cycles") - ? "Cycle" - : props.path?.includes("modules") - ? "Module" - : props.path?.includes("views") - ? "Project view" - : props.path?.includes("inbox") - ? "Inbox" - : props.path?.includes("draft") - ? "Draft" - : "Project", + ? "Cycle" + : props.path?.includes("modules") + ? "Module" + : props.path?.includes("views") + ? "Project view" + : props.path?.includes("inbox") + ? "Inbox" + : props.path?.includes("draft") + ? "Draft" + : "Project", }; } return eventPayload; diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index aa588d9e1a2..50d6c15dfad 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,8 +1,7 @@ -import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; -import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; -import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; import { FC } from "react"; import { ISvgIcons } from "@plane/ui/src/icons/type"; +import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; +import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; import { SpreadsheetAssigneeColumn, SpreadsheetAttachmentColumn, @@ -19,6 +18,7 @@ import { SpreadsheetSubIssueColumn, SpreadsheetUpdatedOnColumn, } from "components/issues/issue-layouts/spreadsheet"; +import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; export const SPREADSHEET_PROPERTY_DETAILS: { [key: string]: { diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 1471de3958c..7ae89d5d6e4 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -1,14 +1,14 @@ // services images -import GithubLogo from "public/services/github.png"; -import JiraLogo from "public/services/jira.svg"; +import { SettingIcon } from "components/icons"; +import { Props } from "components/icons/types"; import CSVLogo from "public/services/csv.svg"; import ExcelLogo from "public/services/excel.svg"; +import GithubLogo from "public/services/github.png"; +import JiraLogo from "public/services/jira.svg"; import JSONLogo from "public/services/json.svg"; // types import { TStaticViewTypes } from "@plane/types"; -import { Props } from "components/icons/types"; // icons -import { SettingIcon } from "components/icons"; export enum EUserWorkspaceRoles { GUEST = 5, diff --git a/web/contexts/user-notification-context.tsx b/web/contexts/user-notification-context.tsx index b55a05771e7..ef3af2124fe 100644 --- a/web/contexts/user-notification-context.tsx +++ b/web/contexts/user-notification-context.tsx @@ -3,9 +3,9 @@ import { createContext, useCallback, useEffect, useReducer } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; import { NotificationService } from "services/notification.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IUserNotification } from "@plane/types"; diff --git a/web/helpers/analytics.helper.ts b/web/helpers/analytics.helper.ts index 58a456ed730..dfa98d7ea67 100644 --- a/web/helpers/analytics.helper.ts +++ b/web/helpers/analytics.helper.ts @@ -1,13 +1,13 @@ // nivo import { BarDatum } from "@nivo/bar"; // helpers +import { DATE_KEYS } from "constants/analytics"; +import { MONTHS_LIST } from "constants/calendar"; +import { STATE_GROUPS } from "constants/state"; import { addSpaceIfCamelCase, capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper"; // types import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; -import { MONTHS_LIST } from "constants/calendar"; -import { DATE_KEYS } from "constants/analytics"; export const convertResponseToBarGraphData = ( response: IAnalyticsData | undefined, @@ -36,8 +36,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(key) - : key, + ? capitalizeFirstLetter(key) + : key, ...segments, }); } else { @@ -49,8 +49,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(item.dimension) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(item.dimension ?? "None") - : item.dimension ?? "None", + ? capitalizeFirstLetter(item.dimension ?? "None") + : item.dimension ?? "None", [yAxisKey]: item[yAxisKey] ?? 0, }); } @@ -84,12 +84,12 @@ export const generateBarColor = ( priority === "urgent" ? "#ef4444" : priority === "high" - ? "#f97316" - : priority === "medium" - ? "#eab308" - : priority === "low" - ? "#22c55e" - : "#ced4da"; + ? "#f97316" + : priority === "medium" + ? "#eab308" + : priority === "low" + ? "#22c55e" + : "#ced4da"; } return color ?? generateRandomColor(value); diff --git a/web/helpers/calendar.helper.ts b/web/helpers/calendar.helper.ts index e570a5c9a34..6c648dd6bbe 100644 --- a/web/helpers/calendar.helper.ts +++ b/web/helpers/calendar.helper.ts @@ -1,7 +1,7 @@ // helpers +import { ICalendarDate, ICalendarPayload } from "components/issues"; import { getWeekNumberOfDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarPayload } from "components/issues"; export const formatDate = (date: Date, format: string): string => { const day = date.getDate(); diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts index 0b30f95e1c7..d31a25b3d6e 100644 --- a/web/helpers/filter.helper.ts +++ b/web/helpers/filter.helper.ts @@ -13,4 +13,3 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number => ) .reduce((curr, prev) => curr + prev, 0) : 0; - diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 831cb321ef6..08cb4abd720 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,8 +1,12 @@ -import { v4 as uuidv4 } from "uuid"; import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; +import { v4 as uuidv4 } from "uuid"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // types +import { IGanttBlock } from "components/gantt-chart"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { orderArrayBy } from "helpers/array.helper"; import { TIssue, TIssueGroupByOptions, @@ -11,10 +15,6 @@ import { TIssueParams, TStateGroups, } from "@plane/types"; -import { IGanttBlock } from "components/gantt-chart"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; type THandleIssuesMutation = ( formData: Partial, diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index d30b29b5231..ad87c2e7502 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -1,10 +1,10 @@ +import * as DOMPurify from "dompurify"; import { CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, VIEW_ISSUES, } from "constants/fetch-keys"; -import * as DOMPurify from 'dompurify'; export const addSpaceIfCamelCase = (str: string) => { if (str === undefined || str === null) return ""; @@ -172,10 +172,10 @@ export const getFetchKeysForIssueMutation = (options: { const ganttFetchKey = cycleId ? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) } : moduleId - ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } - : viewId - ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } - : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; + ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } + : viewId + ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } + : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; return { ...ganttFetchKey, diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 2349b1585a7..ff036a52976 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,5 +1,5 @@ export * from "./use-application"; -export * from "./use-event-tracker" +export * from "./use-event-tracker"; export * from "./use-calendar-view"; export * from "./use-cycle"; export * from "./use-dashboard"; diff --git a/web/hooks/store/use-inbox-issues.ts b/web/hooks/store/use-inbox-issues.ts index 2b2941f84ee..1196eae9014 100644 --- a/web/hooks/store/use-inbox-issues.ts +++ b/web/hooks/store/use-inbox-issues.ts @@ -2,8 +2,8 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "contexts/store-context"; // types -import { IInboxIssue } from "store/inbox/inbox_issue.store"; import { IInboxFilter } from "store/inbox/inbox_filter.store"; +import { IInboxIssue } from "store/inbox/inbox_issue.store"; export const useInboxIssues = (): { issues: IInboxIssue; diff --git a/web/hooks/store/use-issues.ts b/web/hooks/store/use-issues.ts index f2da9d954e0..ed270c9ec9b 100644 --- a/web/hooks/store/use-issues.ts +++ b/web/hooks/store/use-issues.ts @@ -1,19 +1,19 @@ import { useContext } from "react"; import merge from "lodash/merge"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { StoreContext } from "contexts/store-context"; // types -import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; +import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; +import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; +import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; import { TIssueMap } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type defaultIssueStore = { issueMap: TIssueMap; diff --git a/web/hooks/use-comment-reaction.tsx b/web/hooks/use-comment-reaction.tsx index 2327fddcdc8..3750160b054 100644 --- a/web/hooks/use-comment-reaction.tsx +++ b/web/hooks/use-comment-reaction.tsx @@ -2,9 +2,9 @@ import useSWR from "swr"; // fetch keys import { COMMENT_REACTION_LIST } from "constants/fetch-keys"; // services +import { groupReactions } from "helpers/emoji.helper"; import { IssueReactionService } from "services/issue"; // helpers -import { groupReactions } from "helpers/emoji.helper"; import { useUser } from "./store"; // hooks diff --git a/web/hooks/use-draggable-portal.ts b/web/hooks/use-draggable-portal.ts index 383c277f39d..325f8b26803 100644 --- a/web/hooks/use-draggable-portal.ts +++ b/web/hooks/use-draggable-portal.ts @@ -1,6 +1,6 @@ -import { createPortal } from "react-dom"; import { useEffect, useRef } from "react"; import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { createPortal } from "react-dom"; const useDraggableInPortal = () => { const self = useRef(); diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx index 228e3557511..174cfdd8ae4 100644 --- a/web/hooks/use-dropdown-key-down.tsx +++ b/web/hooks/use-dropdown-key-down.tsx @@ -1,9 +1,11 @@ import { useCallback } from "react"; type TUseDropdownKeyDown = { - (onEnterKeyDown: () => void, onEscKeyDown: () => void, stopPropagation?: boolean): ( - event: React.KeyboardEvent - ) => void; + ( + onEnterKeyDown: () => void, + onEscKeyDown: () => void, + stopPropagation?: boolean + ): (event: React.KeyboardEvent) => void; }; export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => { diff --git a/web/hooks/use-user-notifications.tsx b/web/hooks/use-user-notifications.tsx index 3c2ec6332a1..41bb6cbfdee 100644 --- a/web/hooks/use-user-notifications.tsx +++ b/web/hooks/use-user-notifications.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; // services +import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; import { NotificationService } from "services/notification.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types"; // ui diff --git a/web/hooks/use-user.tsx b/web/hooks/use-user.tsx index 35757902611..ffe6c963b10 100644 --- a/web/hooks/use-user.tsx +++ b/web/hooks/use-user.tsx @@ -2,9 +2,9 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { CURRENT_USER } from "constants/fetch-keys"; import { UserService } from "services/user.service"; // constants -import { CURRENT_USER } from "constants/fetch-keys"; // types import type { IUser } from "@plane/types"; diff --git a/web/layouts/admin-layout/header.tsx b/web/layouts/admin-layout/header.tsx index 2607fe91d62..e12875d869b 100644 --- a/web/layouts/admin-layout/header.tsx +++ b/web/layouts/admin-layout/header.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; // mobx import { observer } from "mobx-react-lite"; // ui +import { Settings } from "lucide-react"; import { Breadcrumbs } from "@plane/ui"; // icons -import { Settings } from "lucide-react"; import { BreadcrumbLink } from "components/common"; export interface IInstanceAdminHeader { diff --git a/web/layouts/admin-layout/layout.tsx b/web/layouts/admin-layout/layout.tsx index 2dbcdf1f5df..bd53fc060c6 100644 --- a/web/layouts/admin-layout/layout.tsx +++ b/web/layouts/admin-layout/layout.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceSetupView } from "components/instance"; import { useApplication } from "hooks/store"; // layouts import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout"; // components -import { InstanceAdminSidebar } from "./sidebar"; import { InstanceAdminHeader } from "./header"; -import { InstanceSetupView } from "components/instance"; +import { InstanceAdminSidebar } from "./sidebar"; export interface IInstanceAdminLayout { children: ReactNode; diff --git a/web/layouts/admin-layout/sidebar.tsx b/web/layouts/admin-layout/sidebar.tsx index efd3cfc76a4..2af3f09822b 100644 --- a/web/layouts/admin-layout/sidebar.tsx +++ b/web/layouts/admin-layout/sidebar.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; import { useApplication } from "hooks/store"; // components -import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; export interface IInstanceAdminSidebar {} diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index 07ec9711df7..dd1df164fd6 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,10 +1,13 @@ import { FC, ReactNode } from "react"; // layouts +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +import { CommandPalette } from "components/command-palette"; +import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store/use-issues"; import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout"; // components -import { CommandPalette } from "components/command-palette"; import { AppSidebar } from "./sidebar"; -import { observer } from "mobx-react-lite"; export interface IAppLayout { children: ReactNode; diff --git a/web/layouts/app-layout/sidebar.tsx b/web/layouts/app-layout/sidebar.tsx index c3b47d02189..6ff6f01d7cb 100644 --- a/web/layouts/app-layout/sidebar.tsx +++ b/web/layouts/app-layout/sidebar.tsx @@ -1,13 +1,13 @@ import { FC, useRef } from "react"; import { observer } from "mobx-react-lite"; // components +import { ProjectSidebarList } from "components/project"; import { WorkspaceHelpSection, WorkspaceSidebarDropdown, WorkspaceSidebarMenu, WorkspaceSidebarQuickAction, } from "components/workspace"; -import { ProjectSidebarList } from "components/project"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; diff --git a/web/layouts/auth-layout/admin-wrapper.tsx b/web/layouts/auth-layout/admin-wrapper.tsx index 236d1c440d9..6d44e6f144c 100644 --- a/web/layouts/auth-layout/admin-wrapper.tsx +++ b/web/layouts/auth-layout/admin-wrapper.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceAdminRestriction } from "components/instance"; import { useApplication, useUser } from "hooks/store"; // components -import { InstanceAdminRestriction } from "components/instance"; export interface IAdminAuthWrapper { children: ReactNode; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index bdd2da8b524..fc672c81274 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -1,8 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +// components +import { Spinner } from "@plane/ui"; +import { JoinProject } from "components/auth-screens"; +import { EmptyState } from "components/common"; import { useApplication, useEventTracker, @@ -17,10 +21,6 @@ import { useUser, useInbox, } from "hooks/store"; -// components -import { Spinner } from "@plane/ui"; -import { JoinProject } from "components/auth-screens"; -import { EmptyState } from "components/common"; // images import emptyProject from "public/empty-state/project.svg"; diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index b48e20b1027..2a9502a0b16 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; // hooks +import { Spinner } from "@plane/ui"; import { useUser, useWorkspace } from "hooks/store"; // ui -import { Spinner } from "@plane/ui"; export interface IUserAuthWrapper { children: ReactNode; diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index ba64983012e..199a7e5bc96 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { Button, Spinner } from "@plane/ui"; import { useLabel, useMember, useProject, useUser } from "hooks/store"; // icons -import { Button, Spinner } from "@plane/ui"; export interface IWorkspaceAuthWrapper { children: ReactNode; diff --git a/web/layouts/instance-layout/index.tsx b/web/layouts/instance-layout/index.tsx index d5df476d95a..7e22b7321a1 100644 --- a/web/layouts/instance-layout/index.tsx +++ b/web/layouts/instance-layout/index.tsx @@ -1,12 +1,12 @@ import { FC, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; -// components import { Spinner } from "@plane/ui"; import { InstanceNotReady } from "components/instance"; +import { useApplication } from "hooks/store"; +// components type Props = { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index 5bf5f0eeae1..ed594c9f2b9 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; // layout +import { CommandPalette } from "components/command-palette"; import { UserAuthWrapper } from "layouts/auth-layout"; import { ProfileLayoutSidebar } from "layouts/settings-layout"; // components -import { CommandPalette } from "components/command-palette"; interface IProfileSettingsLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/preferences/index.ts b/web/layouts/settings-layout/profile/preferences/index.ts index 34e23025847..c4bfd4db38a 100644 --- a/web/layouts/settings-layout/profile/preferences/index.ts +++ b/web/layouts/settings-layout/profile/preferences/index.ts @@ -1,2 +1,2 @@ export * from "./layout"; -export * from "./sidebar"; \ No newline at end of file +export * from "./sidebar"; diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 116813958f2..71a1fdd851e 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; // layout -import { ProfileSettingsLayout } from "layouts/settings-layout"; -import { ProfilePreferenceSettingsSidebar } from "./sidebar"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { CustomMenu } from "@plane/ui"; -import { ChevronDown } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { ChevronDown } from "lucide-react"; +import { CustomMenu } from "@plane/ui"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { useApplication } from "hooks/store"; +import { ProfileSettingsLayout } from "layouts/settings-layout"; +import { ProfilePreferenceSettingsSidebar } from "./sidebar"; interface IProfilePreferenceSettingsLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx index 7f43f3cad17..27b28905ba6 100644 --- a/web/layouts/settings-layout/profile/preferences/sidebar.tsx +++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; export const ProfilePreferenceSettingsSidebar = () => { const router = useRouter(); @@ -9,15 +9,15 @@ export const ProfilePreferenceSettingsSidebar = () => { label: string; href: string; }> = [ - { - label: "Theme", - href: `/profile/preferences/theme`, - }, - { - label: "Email", - href: `/profile/preferences/email`, - }, - ]; + { + label: "Theme", + href: `/profile/preferences/theme`, + }, + { + label: "Email", + href: `/profile/preferences/email`, + }, + ]; return (
@@ -26,10 +26,11 @@ export const ProfilePreferenceSettingsSidebar = () => { {profilePreferenceLinks.map((link) => (
{link.label}
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 4d78195f139..caa5cd56e1e 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -1,9 +1,9 @@ import { useEffect, useRef, useState } from "react"; -import { mutate } from "swr"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; @@ -11,7 +11,9 @@ import { useApplication, useUser, useWorkspace } from "hooks/store"; import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; +import { useApplication, useUser, useWorkspace } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useToast from "hooks/use-toast"; const WORKSPACE_ACTION_LINKS = [ { diff --git a/web/layouts/settings-layout/project/layout.tsx b/web/layouts/settings-layout/project/layout.tsx index 38525e98cd7..1ea4c23222b 100644 --- a/web/layouts/settings-layout/project/layout.tsx +++ b/web/layouts/settings-layout/project/layout.tsx @@ -1,16 +1,16 @@ import { FC, ReactNode } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useUser } from "hooks/store"; // components -import { ProjectSettingsSidebar } from "./sidebar"; +import { Button, LayersIcon } from "@plane/ui"; import { NotAuthorizedView } from "components/auth-screens"; // ui -import { Button, LayersIcon } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; +import { useUser } from "hooks/store"; +import { ProjectSettingsSidebar } from "./sidebar"; export interface IProjectSettingLayout { children: ReactNode; diff --git a/web/layouts/settings-layout/project/sidebar.tsx b/web/layouts/settings-layout/project/sidebar.tsx index 054add4ee37..8cf2befc279 100644 --- a/web/layouts/settings-layout/project/sidebar.tsx +++ b/web/layouts/settings-layout/project/sidebar.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui import { Loader } from "@plane/ui"; // hooks +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { useUser } from "hooks/store"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; export const ProjectSettingsSidebar = () => { const router = useRouter(); diff --git a/web/layouts/settings-layout/workspace/sidebar.tsx b/web/layouts/settings-layout/workspace/sidebar.tsx index c8d4718c706..f5177139b46 100644 --- a/web/layouts/settings-layout/workspace/sidebar.tsx +++ b/web/layouts/settings-layout/workspace/sidebar.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; import { useUser } from "hooks/store"; // constants -import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; export const WorkspaceSettingsSidebar = () => { // router diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 52bfc6fbf8e..243eaed1a36 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,6 +1,7 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { ProfileNavbar, ProfileSidebar } from "components/profile"; import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index a917936138d..8fcb617449e 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -1,26 +1,24 @@ import { FC, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import Router from "next/router"; -import NProgress from "nprogress"; -import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; -// hooks -import { useApplication, useUser, useWorkspace } from "hooks/store"; +import NProgress from "nprogress"; +import { SWRConfig } from "swr"; // ui import { Toast } from "@plane/ui"; // constants import { SWR_CONFIG } from "constants/swr-config"; -// layouts -import InstanceLayout from "layouts/instance-layout"; -// contexts -import { SWRConfig } from "swr"; //helpers import { resolveGeneralTheme } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser, useWorkspace } from "hooks/store"; +// layouts +import InstanceLayout from "layouts/instance-layout"; // dynamic imports const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("lib/posthog-provider"), { ssr: false }); const CrispWrapper = dynamic(() => import("lib/wrappers/crisp-wrapper"), { ssr: false }); - // nprogress NProgress.configure({ showSpinner: false }); Router.events.on("routeChangeStart", NProgress.start); diff --git a/web/lib/local-storage.ts b/web/lib/local-storage.ts index e0d77dc516f..ab84b358f57 100644 --- a/web/lib/local-storage.ts +++ b/web/lib/local-storage.ts @@ -3,15 +3,15 @@ import isEmpty from "lodash/isEmpty"; export const storage = { set: (key: string, value: object | string | boolean): void => { if (typeof window === undefined || typeof window === "undefined" || !key || !value) return undefined; - const _value: string | undefined = value + const tempValue: string | undefined = value ? ["string", "boolean"].includes(typeof value) ? value.toString() : isEmpty(value) - ? undefined - : JSON.stringify(value) + ? undefined + : JSON.stringify(value) : undefined; - if (!_value) return undefined; - window.localStorage.setItem(key, _value); + if (!tempValue) return undefined; + window.localStorage.setItem(key, tempValue); }, get: (key: string): string | undefined => { diff --git a/web/lib/posthog-provider.tsx b/web/lib/posthog-provider.tsx index c5acd295761..80391ba95f1 100644 --- a/web/lib/posthog-provider.tsx +++ b/web/lib/posthog-provider.tsx @@ -2,12 +2,12 @@ import { FC, ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; -// mobx store provider -import { IUser } from "@plane/types"; -// helpers -import { getUserRole } from "helpers/user.helper"; // constants import { GROUP_WORKSPACE } from "constants/event-tracker"; +// helpers +import { getUserRole } from "helpers/user.helper"; +// types +import { IUser } from "@plane/types"; export interface IPosthogWrapper { children: ReactNode; @@ -59,7 +59,7 @@ const PostHogProvider: FC = (props) => { posthog?.identify(user.email); posthog?.group(GROUP_WORKSPACE, currentWorkspaceId); } - }, [currentWorkspaceId, user]); + }, [currentWorkspaceId, lastWorkspaceId, user]); useEffect(() => { // Track page views diff --git a/web/lib/types.d.ts b/web/lib/types.d.ts index 2b03f6975b9..8dac1ff8259 100644 --- a/web/lib/types.d.ts +++ b/web/lib/types.d.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/ban-types export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; diff --git a/web/lib/wrappers/crisp-wrapper.tsx b/web/lib/wrappers/crisp-wrapper.tsx index beacf916bb5..d2771abd846 100644 --- a/web/lib/wrappers/crisp-wrapper.tsx +++ b/web/lib/wrappers/crisp-wrapper.tsx @@ -4,8 +4,8 @@ import { IUser } from "@plane/types"; declare global { interface Window { - $crisp: any; - CRISP_WEBSITE_ID: any; + $crisp: unknown[]; + CRISP_WEBSITE_ID: unknown; } } @@ -22,8 +22,8 @@ const CrispWrapper: FC = (props) => { window.$crisp = []; window.CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_ID; (function () { - var d = document; - var s = d.createElement("script"); + const d = document; + const s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = true; d.getElementsByTagName("head")[0].appendChild(s); diff --git a/web/lib/wrappers/store-wrapper.tsx b/web/lib/wrappers/store-wrapper.tsx index 83867f557d6..1890bba5072 100644 --- a/web/lib/wrappers/store-wrapper.tsx +++ b/web/lib/wrappers/store-wrapper.tsx @@ -1,12 +1,12 @@ import { ReactNode, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -// hooks -import { useApplication, useUser } from "hooks/store"; +import useSWR from "swr"; // helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser } from "hooks/store"; interface IStoreWrapper { children: ReactNode; @@ -15,7 +15,7 @@ interface IStoreWrapper { const StoreWrapper: FC = observer((props) => { const { children } = props; // states - const [dom, setDom] = useState(); + const [dom, setDom] = useState(); // router const router = useRouter(); // store hooks diff --git a/web/package.json b/web/package.json index fbec571ef8f..99e35119177 100644 --- a/web/package.json +++ b/web/package.json @@ -70,11 +70,7 @@ "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.17", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.48.2", - "@typescript-eslint/parser": "^5.48.2", - "eslint": "^8.31.0", "eslint-config-custom": "*", - "eslint-config-next": "12.2.2", "prettier": "^2.8.7", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/web/pages/404.tsx b/web/pages/404.tsx index a73cd2074db..639a773334a 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -1,17 +1,17 @@ import React from "react"; -import Link from "next/link"; +import type { NextPage } from "next"; import Image from "next/image"; +import Link from "next/link"; // components +import { Button } from "@plane/ui"; import { PageHead } from "components/core"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; // images import Image404 from "public/404.svg"; // types -import type { NextPage } from "next"; const PageNotFound: NextPage = () => ( diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/pages/[workspaceSlug]/active-cycles.tsx index f366ddbd6c0..b7e3b41009e 100644 --- a/web/pages/[workspaceSlug]/active-cycles.tsx +++ b/web/pages/[workspaceSlug]/active-cycles.tsx @@ -5,11 +5,11 @@ import { PageHead } from "components/core"; import { WorkspaceActiveCycleHeader } from "components/headers"; import { WorkspaceActiveCyclesUpgrade } from "components/workspace"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useWorkspace } from "hooks/store"; const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 31c396b54af..658f3e34c4e 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,21 +1,21 @@ import React, { Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Tab } from "@headlessui/react"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { Tab } from "@headlessui/react"; // hooks -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; -import { WorkspaceAnalyticsHeader } from "components/headers"; +import { PageHead } from "components/core"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceAnalyticsHeader } from "components/headers"; // constants import { ANALYTICS_TABS } from "constants/analytics"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/index.tsx b/web/pages/[workspaceSlug]/index.tsx index 8a6782de889..0011e26199f 100644 --- a/web/pages/[workspaceSlug]/index.tsx +++ b/web/pages/[workspaceSlug]/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { WorkspaceDashboardView } from "components/page-views"; import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard"; +import { WorkspaceDashboardView } from "components/page-views"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspacePage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx index 09269676a8d..87029724ed2 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -1,20 +1,20 @@ import { ReactElement, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks +import { Button } from "@plane/ui"; +import { UserProfileHeader } from "components/headers"; +import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const PER_PAGE = 100; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index 1cef81e7843..9d1dbf07238 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,13 +1,13 @@ import React, { ReactElement } from "react"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileAssignedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index 47a8445d7ce..105d9d309af 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileCreatedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index 6e8a10b5073..eb71989ed4f 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -2,13 +2,10 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services -import { UserService } from "services/user.service"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; import { ProfileActivity, ProfilePriorityDistribution, @@ -17,11 +14,14 @@ import { ProfileWorkload, } from "components/profile"; // types -import { IUserStateDistribution, TStateGroups } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants import { USER_PROFILE_DATA } from "constants/fetch-keys"; import { GROUP_CHOICES } from "constants/project"; +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +import { NextPageWithLayout } from "lib/types"; +import { UserService } from "services/user.service"; +import { IUserStateDistribution, TStateGroups } from "@plane/types"; // services const userService = new UserService(); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index c05c3930292..c81ed69189f 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileSubscribedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index 34019c02632..353f0a8b69b 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { ProjectArchivedIssuesHeader } from "components/headers"; +import { ArchivedIssueLayoutRoot } from "components/issues"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // contexts -import { ArchivedIssueLayoutRoot } from "components/issues"; // components -import { ProjectArchivedIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 7b5ec883312..6eaef6c0fcf 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,23 +1,23 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { CycleDetailsSidebar } from "components/cycles"; +import { CycleIssuesHeader } from "components/headers"; +import { CycleLayoutRoot } from "components/issues/issue-layouts"; import { useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { CycleIssuesHeader } from "components/headers"; -import { CycleDetailsSidebar } from "components/cycles"; -import { CycleLayoutRoot } from "components/issues/issue-layouts"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyCycle from "public/empty-state/cycle.svg"; // types -import { NextPageWithLayout } from "lib/types"; const CycleDetailPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 0f86089aa69..ac2b760ef11 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,28 +1,28 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Tab } from "@headlessui/react"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { Tab } from "@headlessui/react"; // hooks +import { Tooltip } from "@plane/ui"; +import { PageHead } from "components/core"; +import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { CyclesHeader } from "components/headers"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { CyclesHeader } from "components/headers"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Tooltip } from "@plane/ui"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // types -import { TCycleView, TCycleLayout } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; +import { TCycleView, TCycleLayout } from "@plane/types"; // constants -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx index bf11063c3b2..c506e55b0c9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { X, PenSquare } from "lucide-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; import { PageHead } from "components/core"; import { ProjectDraftIssueHeader } from "components/headers"; +import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index de412c9d7dd..f8fb1aa4747 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectInboxHeader } from "components/headers"; +import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; +import { InboxLayoutLoader } from "components/ui"; import { useProject, useInboxIssues } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { InboxLayoutLoader } from "components/ui"; -import { PageHead } from "components/core"; -import { ProjectInboxHeader } from "components/headers"; -import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx index 1021ad1020e..c3d3f2e5a58 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { ProjectInboxHeader } from "components/headers"; +import { InboxLayoutLoader } from "components/ui"; import { useInbox, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // ui -import { InboxLayoutLoader } from "components/ui"; // components -import { ProjectInboxHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 6ff7d5aa512..54994ab6d15 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -1,19 +1,19 @@ import React, { ReactElement, useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts -import { AppLayout } from "layouts/app-layout"; -// components +import { Loader } from "@plane/ui"; import { PageHead } from "components/core"; +// components import { ProjectIssueDetailsHeader } from "components/headers"; import { IssueDetailRoot } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types -import { NextPageWithLayout } from "lib/types"; // store hooks import { useApplication, useIssueDetail, useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 2aa9ab2e6a2..241af79c4a5 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import Head from "next/head"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // components -import { ProjectLayoutRoot } from "components/issues"; +import { PageHead } from "components/core"; import { ProjectIssuesHeader } from "components/headers"; +import { ProjectLayoutRoot } from "components/issues"; // types +import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; import { NextPageWithLayout } from "lib/types"; // layouts -import { AppLayout } from "layouts/app-layout"; // hooks -import { useProject } from "hooks/store"; -import { PageHead } from "components/core"; const ProjectIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index afbd97b8e22..e55eea17039 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,22 +1,22 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ModuleIssuesHeader } from "components/headers"; +import { ModuleLayoutRoot } from "components/issues"; +import { ModuleDetailsSidebar } from "components/modules"; import { useModule, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ModuleDetailsSidebar } from "components/modules"; -import { ModuleLayoutRoot } from "components/issues"; -import { ModuleIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyModule from "public/empty-state/module.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ModuleIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 085f1e3c3bc..3648f592253 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { ModulesListView } from "components/modules"; import { ModulesListHeader } from "components/headers"; +import { ModulesListView } from "components/modules"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectModulesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index c44f6186ef7..3a133ee5098 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -1,33 +1,33 @@ -import { Sparkle } from "lucide-react"; +import { ReactElement, useEffect, useRef, useState } from "react"; +import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import { useRouter } from "next/router"; -import { ReactElement, useEffect, useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; +import useSWR from "swr"; +import { Sparkle } from "lucide-react"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +import { GptAssistantPopover, PageHead } from "components/core"; +import { PageDetailsHeader } from "components/headers/page-details"; +import { IssuePeekOverview } from "components/issues"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; import useReloadConfirmations from "hooks/use-reload-confirmation"; // services +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; import { FileService } from "services/file.service"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { GptAssistantPopover, PageHead } from "components/core"; -import { PageDetailsHeader } from "components/headers/page-details"; // ui -import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // assets // helpers // types import { IPage } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys // constants -import { EUserProjectRoles } from "constants/project"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; -import { IssuePeekOverview } from "components/issues"; // services const fileService = new FileService(); @@ -311,7 +311,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { updatePageTitle={updatePageTitle} onActionCompleteHandler={actionCompleteAlert} customClassName="tracking-tight self-center h-full w-full right-[0.675rem]" - onChange={(_description_json: Object, description_html: string) => { + onChange={(_description_json: any, description_html: string) => { setShowAlert(true); onChange(description_html); handleSubmit(updatePage)(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index a8c85ef8dc5..45204541bcb 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -1,30 +1,30 @@ import { useState, Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; -import dynamic from "next/dynamic"; -import { Tab } from "@headlessui/react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import dynamic from "next/dynamic"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; +import { Tab } from "@headlessui/react"; // hooks +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PagesHeader } from "components/headers"; +import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { PagesLoader } from "components/ui"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PAGE_TABS_LIST } from "constants/page"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; import useUserAuth from "hooks/use-user-auth"; import useSize from "hooks/use-window-size"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesHeader } from "components/headers"; -import { PagesLoader } from "components/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { PAGE_TABS_LIST } from "constants/page"; -import { useProjectPages } from "hooks/store/use-project-page"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 1cefb941831..d6724c78919 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -1,22 +1,25 @@ import React, { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useProject, useUser } from "hooks/store"; +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; // ui -import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; +import { useProject, useUser } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +// layouts +import { ProjectSettingLayout } from "layouts/settings-layout"; +// hooks +// components // types import { NextPageWithLayout } from "lib/types"; import { IProject } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; const AutomationSettingsPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 70108f90a0b..c1aea645f65 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,18 +1,18 @@ import { ReactElement } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { EstimatesList } from "components/estimates"; +import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; import { useUser, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { EstimatesList } from "components/estimates"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserProjectRoles } from "constants/project"; const EstimatesSettingsPage: NextPageWithLayout = observer(() => { const { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index b618437abd0..e36ebd9a80d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectFeaturesList } from "components/project"; import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { ProjectFeaturesList } from "components/project"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 347d64f8440..037e4743479 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -1,13 +1,8 @@ import { useState, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; -// components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { @@ -16,6 +11,11 @@ import { ProjectDetailsForm, ProjectDetailsFormLoader, } from "components/project"; +import { useProject } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +// components // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 5c9faae7cda..b227becf955 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,29 +1,29 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { useTheme } from "next-themes"; -import { observer } from "mobx-react"; +import useSWR from "swr"; // hooks +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ProjectSettingHeader } from "components/headers"; +import { IntegrationCard } from "components/project"; +import { IntegrationsSettingsLoader } from "components/ui"; +import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // services +import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components -import { PageHead } from "components/core"; -import { IntegrationCard } from "components/project"; -import { ProjectSettingHeader } from "components/headers"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { IntegrationsSettingsLoader } from "components/ui"; // types import { IProject } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys -import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; // services const integrationService = new IntegrationService(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index d62ac1e6653..8b375882914 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingsLabelList } from "components/labels"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingsLabelList } from "components/labels"; -import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const LabelsSettingsPage: NextPageWithLayout = observer(() => { const { currentProjectDetails } = useProject(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index f74d464d591..551dde0c265 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; // components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; const MembersSettingsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 57451e69992..4a5c290d8a8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layout +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingStateList } from "components/states"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { ProjectSettingStateList } from "components/states"; -import { ProjectSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // hook -import { useProject } from "hooks/store"; const StatesSettingsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 2ac6b2e0046..17ba29394e8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -1,21 +1,21 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ProjectViewIssuesHeader } from "components/headers"; +import { ProjectViewLayoutRoot } from "components/issues"; import { useProject, useProjectView } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ProjectViewLayoutRoot } from "components/issues"; -import { ProjectViewIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyView from "public/empty-state/view.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ProjectViewIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 33be5d10201..9864ef391a4 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,10 +1,10 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // components +import { PageHead } from "components/core"; import { ProjectViewsHeader } from "components/headers"; import { ProjectViewsList } from "components/views"; -import { PageHead } from "components/core"; // hooks import { useProject } from "hooks/store"; // layouts diff --git a/web/pages/[workspaceSlug]/projects/index.tsx b/web/pages/[workspaceSlug]/projects/index.tsx index 1a145a2d1c9..158e6577f96 100644 --- a/web/pages/[workspaceSlug]/projects/index.tsx +++ b/web/pages/[workspaceSlug]/projects/index.tsx @@ -2,13 +2,13 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // components import { PageHead } from "components/core"; -import { ProjectCardList } from "components/project"; import { ProjectsHeader } from "components/headers"; +import { ProjectCardList } from "components/project"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; -import { useWorkspace } from "hooks/store"; const ProjectsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 35366cb0a20..75d46b63df5 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,29 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // store hooks +import { Button } from "@plane/ui"; +import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceSettingHeader } from "components/headers"; +import { APITokenSettingsLoader } from "components/ui"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { API_TOKENS_LIST } from "constants/fetch-keys"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component -import { WorkspaceSettingHeader } from "components/headers"; -import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { APITokenSettingsLoader } from "components/ui"; // services +import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types -import { NextPageWithLayout } from "lib/types"; // constants -import { API_TOKENS_LIST } from "constants/fetch-keys"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const apiTokenService = new APITokenService(); diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index f4f5d5397dd..bd1114f85d5 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -1,18 +1,18 @@ import { observer } from "mobx-react-lite"; // hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const BillingSettingsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index c124a642392..a6f95847227 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import ExportGuide from "components/exporter/guide"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layout import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import ExportGuide from "components/exporter/guide"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ExportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index 5178209d272..19eeeac66e7 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import IntegrationGuide from "components/integration/guide"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts -import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import IntegrationGuide from "components/integration/guide"; -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ImportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 2924b13c4a8..37ce39335d3 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -1,14 +1,14 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WorkspaceDetails } from "components/workspace"; +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // hooks -import { useWorkspace } from "hooks/store"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { WorkspaceDetails } from "components/workspace"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/settings/integrations.tsx b/web/pages/[workspaceSlug]/settings/integrations.tsx index 500533877a4..0aa54f60a0a 100644 --- a/web/pages/[workspaceSlug]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/settings/integrations.tsx @@ -1,26 +1,26 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks -import { useUser, useWorkspace } from "hooks/store"; // services -import { IntegrationService } from "services/integrations"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { SingleIntegrationCard } from "components/integration"; -import { WorkspaceSettingHeader } from "components/headers"; import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SingleIntegrationCard } from "components/integration"; // ui import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui"; // types -import { NextPageWithLayout } from "lib/types"; // fetch-keys import { APP_INTEGRATIONS } from "constants/fetch-keys"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { useUser, useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; +import { IntegrationService } from "services/integrations"; const integrationService = new IntegrationService(); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index f635588c21d..e1be1d889a6 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,26 +1,26 @@ import { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Search } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; +import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; -import { PageHead } from "components/core"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; // helpers -import { getUserRole } from "helpers/user.helper"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { MEMBER_INVITED } from "constants/event-tracker"; const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { // states @@ -30,7 +30,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store hooks - const { captureEvent, setTrackElement } = useEventTracker(); + const { captureEvent } = useEventTracker(); const { membership: { currentWorkspaceRole }, } = useUser(); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index bafaa3aaad7..263f90963b1 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -1,18 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; -import { PageHead } from "components/core"; // ui -import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWebhook } from "@plane/types"; diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 19f23913efe..d5058e29f2b 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,25 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import useSWR from "swr"; // hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; +import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; +import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Button } from "@plane/ui"; -import { WebhookSettingsLoader } from "components/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index 85e90748115..7d736e8f96e 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { AllIssueLayoutRoot } from "components/issues"; +import { GlobalViewsHeader } from "components/workspace"; +import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; +import { useGlobalView, useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // hooks -import { useGlobalView, useWorkspace } from "hooks/store"; // components -import { GlobalViewsHeader } from "components/workspace"; -import { AllIssueLayoutRoot } from "components/issues"; -import { GlobalIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { // router @@ -29,8 +29,8 @@ const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { currentWorkspace?.name && defaultView?.label ? `${currentWorkspace?.name} - ${defaultView?.label}` : currentWorkspace?.name && globalViewDetails?.name - ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` - : undefined; + ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` + : undefined; return ( <> diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index 61fdcf05822..ccd7ac485d8 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -1,21 +1,21 @@ import React, { useState, ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; -import { GlobalIssuesHeader } from "components/headers"; // ui +import { Search } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Search } from "lucide-react"; +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; // types -import { NextPageWithLayout } from "lib/types"; // constants import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspaceViewsPage: NextPageWithLayout = observer(() => { const [query, setQuery] = useState(""); diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index cc0411068ca..ccaa34a4080 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -1,5 +1,6 @@ import Document, { Html, Head, Main, NextScript } from "next/document"; // constants +import Script from "next/script"; import { SITE_NAME, SITE_DESCRIPTION, @@ -8,7 +9,6 @@ import { SITE_KEYWORDS, SITE_TITLE, } from "constants/seo-variables"; -import Script from "next/script"; class MyDocument extends Document { render() { diff --git a/web/pages/_error.tsx b/web/pages/_error.tsx index 0a530cf9f37..81e0daecd69 100644 --- a/web/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -3,11 +3,12 @@ import * as Sentry from "@sentry/nextjs"; import { useRouter } from "next/router"; // services +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + +import DefaultLayout from "layouts/default-layout"; import { AuthService } from "services/auth.service"; // layouts -import DefaultLayout from "layouts/default-layout"; // ui -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services const authService = new AuthService(); diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx index cba9c0166e8..c40a1660ba5 100644 --- a/web/pages/accounts/sign-up.tsx +++ b/web/pages/accounts/sign-up.tsx @@ -1,19 +1,19 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignUpRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components -import { SignUpRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // assets +import { NextPageWithLayout } from "lib/types"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types -import { NextPageWithLayout } from "lib/types"; const SignUpPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 952ed0b6874..629e4a379a5 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -1,23 +1,23 @@ import React, { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; -import Image from "next/image"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; // hooks +import { PageHead } from "components/core"; +import { CreateWorkspaceForm } from "components/workspace"; import { useUser } from "hooks/store"; // layouts -import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; // components -import { CreateWorkspaceForm } from "components/workspace"; -import { PageHead } from "components/core"; // images +import { NextPageWithLayout } from "lib/types"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; // types import { IWorkspace } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; const CreateWorkspacePage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/god-mode/ai.tsx b/web/pages/god-mode/ai.tsx index b84e98098d3..35b652d9b67 100644 --- a/web/pages/god-mode/ai.tsx +++ b/web/pages/god-mode/ai.tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Lightbulb } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceAIForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // icons -import { Lightbulb } from "lucide-react"; // components -import { InstanceAIForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAIPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index 6274fca2047..f4eeefc65ad 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -1,18 +1,19 @@ import { ReactElement, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts +import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/email.tsx b/web/pages/god-mode/email.tsx index 65889607fa6..0e4a594cea3 100644 --- a/web/pages/god-mode/email.tsx +++ b/web/pages/god-mode/email.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceEmailForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceEmailForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/image.tsx b/web/pages/god-mode/image.tsx index 349dccf4bf6..4c6abaa9651 100644 --- a/web/pages/god-mode/image.tsx +++ b/web/pages/god-mode/image.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceImageConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceImageConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminImagePage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/index.tsx b/web/pages/god-mode/index.tsx index a93abad3105..a7cb29c05e7 100644 --- a/web/pages/god-mode/index.tsx +++ b/web/pages/god-mode/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceGeneralForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceGeneralForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 2f8b32394cc..d9e99811f36 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,8 +1,8 @@ import { ReactElement } from "react"; // layouts +import { SignInView } from "components/page-views"; import DefaultLayout from "layouts/default-layout"; // components -import { SignInView } from "components/page-views"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index 85bf2153995..052782dc5c6 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -1,11 +1,11 @@ import React, { useEffect, ReactElement } from "react"; import { useRouter } from "next/router"; // services -import { AppInstallationService } from "services/app_installation.service"; // ui import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; +import { AppInstallationService } from "services/app_installation.service"; // services const appInstallationService = new AppInstallationService(); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 18441f0a0dc..7f976865b6d 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -77,7 +77,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { workspaceService .joinWorkspaces({ invitations: invitationsRespond }) - .then((res) => { + .then(() => { mutate("USER_WORKSPACES"); const firstInviteId = invitationsRespond[0]; const invitation = invitations?.find((i) => i.id === firstInviteId); @@ -85,6 +85,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { joinWorkspaceMetricGroup(redirectWorkspace?.id); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain role: getUserRole(invitation?.role!), project_id: undefined, accepted_from: "App", diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 5b5b91280f6..2ebd61f3a83 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -1,32 +1,32 @@ import { useEffect, useState, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { useTheme } from "next-themes"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; +import { Controller, useForm } from "react-hook-form"; import useSWR from "swr"; -import { ChevronDown } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; -import { Controller, useForm } from "react-hook-form"; +import { ChevronDown } from "lucide-react"; // hooks +import { Avatar, Spinner } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; +import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services +import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; +import { NextPageWithLayout } from "lib/types"; +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { WorkspaceService } from "services/workspace.service"; // layouts -import DefaultLayout from "layouts/default-layout"; -import { UserAuthWrapper } from "layouts/auth-layout"; // components -import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; -import { PageHead } from "components/core"; // ui -import { Avatar, Spinner } from "@plane/ui"; // images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, TOnboardingSteps } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants -import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; // services const workspaceService = new WorkspaceService(); @@ -166,8 +166,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : currentUser?.email + ? value + : currentUser?.email } src={currentUser?.avatar} size={35} @@ -182,8 +182,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { {currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : null} + ? value + : null}

)} diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index b0e8bb1a028..bda1295cf51 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,15 +1,15 @@ import { ReactElement, useState } from "react"; import { observer } from "mobx-react"; //hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ProfileActivityListPage } from "components/profile"; import { useApplication } from "hooks/store"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { ProfileActivityListPage } from "components/profile"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index f37a2b6a65a..7e934475314 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -1,20 +1,20 @@ import { ReactElement, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { useApplication, useUser } from "hooks/store"; // services -import { UserService } from "services/user.service"; // components -import { PageHead } from "components/core"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; // ui -import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { UserService } from "services/user.service"; interface FormValues { old_password: string; @@ -85,8 +85,8 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
= { avatar: "", @@ -143,8 +148,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
@@ -167,7 +172,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { )} /> setDeactivateAccountModal(false)} /> -
+
@@ -303,7 +308,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { ref={ref} hasError={Boolean(errors.email)} placeholder="Enter your email" - className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${ + className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${ errors.email ? "border-red-500" : "" }`} disabled diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index ddd23abdfcc..b34a493e51e 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import useSWR from "swr"; // layouts +import { PageHead } from "components/core"; +import { EmailNotificationForm } from "components/profile/preferences"; +import { EmailSettingsLoader } from "components/ui"; import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // ui -import { EmailSettingsLoader } from "components/ui"; // components -import { EmailNotificationForm } from "components/profile/preferences"; -import { PageHead } from "components/core"; // services +import { NextPageWithLayout } from "lib/types"; import { UserService } from "services/user.service"; // type -import { NextPageWithLayout } from "lib/types"; // services const userService = new UserService(); diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index 94540aeda9c..e23e94c66c6 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -1,16 +1,16 @@ import { useEffect, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; -// hooks -import { useUser } from "hooks/store"; -// layouts -import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; -// components -import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; // ui import { Spinner, setPromiseToast } from "@plane/ui"; +// components +import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; // constants import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; +// hooks +import { useUser } from "hooks/store"; +// layouts +import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // type import { NextPageWithLayout } from "lib/types"; @@ -54,7 +54,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { <> {currentUser ? ( -
+

Preferences

diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx index aa95d0a3829..74c8811254c 100644 --- a/web/pages/workspace-invitations/index.tsx +++ b/web/pages/workspace-invitations/index.tsx @@ -1,22 +1,22 @@ import React, { ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { Spinner } from "@plane/ui"; +import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; +import { WORKSPACE_INVITATION } from "constants/fetch-keys"; import { useUser } from "hooks/store"; // services -import { WorkspaceService } from "services/workspace.service"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Spinner } from "@plane/ui"; // icons -import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; // types import { NextPageWithLayout } from "lib/types"; +import { WorkspaceService } from "services/workspace.service"; // constants -import { WORKSPACE_INVITATION } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); diff --git a/web/services/ai.service.ts b/web/services/ai.service.ts index 11c489c1f30..677f50e92cd 100644 --- a/web/services/ai.service.ts +++ b/web/services/ai.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IGptResponse } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AIService extends APIService { constructor() { diff --git a/web/services/analytics.service.ts b/web/services/analytics.service.ts index 5e3aac44b57..972fe36ea59 100644 --- a/web/services/analytics.service.ts +++ b/web/services/analytics.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { @@ -9,7 +10,6 @@ import { ISaveAnalyticsFormData, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AnalyticsService extends APIService { constructor() { diff --git a/web/services/api_token.service.ts b/web/services/api_token.service.ts index 76a24798f36..3979f6e1f50 100644 --- a/web/services/api_token.service.ts +++ b/web/services/api_token.service.ts @@ -1,6 +1,6 @@ import { API_BASE_URL } from "helpers/common.helper"; -import { APIService } from "./api.service"; import { IApiToken } from "@plane/types"; +import { APIService } from "./api.service"; export class APITokenService extends APIService { constructor() { diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 4b45e0cc467..7c2d1e24e46 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { IAppConfig } from "@plane/types"; diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 17972103640..055a4b0913a 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AppInstallationService extends APIService { constructor() { diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index f47a5282428..f90fafc6628 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IEmailCheckData, diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 5e13e3b8e5b..f7ee8a0ab85 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { CycleDateCheckData, ICycle, TIssue } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class CycleService extends APIService { constructor() { diff --git a/web/services/dashboard.service.ts b/web/services/dashboard.service.ts index e001f92a19a..b1138899d7e 100644 --- a/web/services/dashboard.service.ts +++ b/web/services/dashboard.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { THomeDashboardResponse, TWidget, TWidgetStatsResponse, TWidgetStatsRequestParams } from "@plane/types"; diff --git a/web/services/file.service.ts b/web/services/file.service.ts index d5e80dd536b..0818bc99212 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -1,8 +1,8 @@ // services +import axios from "axios"; +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; -import axios from "axios"; export interface UnSplashImage { id: string; diff --git a/web/services/inbox.service.ts b/web/services/inbox.service.ts index a36d356ce2f..45f0172fb03 100644 --- a/web/services/inbox.service.ts +++ b/web/services/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IInboxIssue, IInbox, TInboxStatus, IInboxQueryParams } from "@plane/types"; diff --git a/web/services/inbox/inbox-issue.service.ts b/web/services/inbox/inbox-issue.service.ts index 6b2099059fe..e6d52768c49 100644 --- a/web/services/inbox/inbox-issue.service.ts +++ b/web/services/inbox/inbox-issue.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types"; diff --git a/web/services/inbox/inbox.service.ts b/web/services/inbox/inbox.service.ts index 8ee6ee51456..fc5fa5a99c8 100644 --- a/web/services/inbox/inbox.service.ts +++ b/web/services/inbox/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInbox } from "@plane/types"; diff --git a/web/services/instance.service.ts b/web/services/instance.service.ts index 1bc5ecdbcee..f61370a918c 100644 --- a/web/services/instance.service.ts +++ b/web/services/instance.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types"; diff --git a/web/services/integrations/github.service.ts b/web/services/integrations/github.service.ts index 6a05195652a..5c4c95c09dd 100644 --- a/web/services/integrations/github.service.ts +++ b/web/services/integrations/github.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IGithubRepoInfo, IGithubServiceImportFormData } from "@plane/types"; diff --git a/web/services/integrations/integration.service.ts b/web/services/integrations/integration.service.ts index 460dc17d926..a1bb10078f5 100644 --- a/web/services/integrations/integration.service.ts +++ b/web/services/integrations/integration.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IAppIntegration, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IntegrationService extends APIService { constructor() { diff --git a/web/services/integrations/jira.service.ts b/web/services/integrations/jira.service.ts index 5641bb28b47..8c254bbab4a 100644 --- a/web/services/integrations/jira.service.ts +++ b/web/services/integrations/jira.service.ts @@ -1,5 +1,5 @@ -import { APIService } from "services/api.service"; import { API_BASE_URL } from "helpers/common.helper"; +import { APIService } from "services/api.service"; // types import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "@plane/types"; diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index 316288278fb..d7f92f79258 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // type import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueService extends APIService { constructor() { diff --git a/web/services/issue/issue_activity.service.ts b/web/services/issue/issue_activity.service.ts index 87c7a8f5414..9028568addc 100644 --- a/web/services/issue/issue_activity.service.ts +++ b/web/services/issue/issue_activity.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueActivityService extends APIService { constructor() { diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index e2a5132a586..e232e796ff3 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssue } from "@plane/types"; // constants -import { API_BASE_URL } from "helpers/common.helper"; export class IssueArchiveService extends APIService { constructor() { diff --git a/web/services/issue/issue_attachment.service.ts b/web/services/issue/issue_attachment.service.ts index 16253218a58..00673c96328 100644 --- a/web/services/issue/issue_attachment.service.ts +++ b/web/services/issue/issue_attachment.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { TIssueAttachment } from "@plane/types"; diff --git a/web/services/issue/issue_comment.service.ts b/web/services/issue/issue_comment.service.ts index 8001d644a44..d7ef35df7c2 100644 --- a/web/services/issue/issue_comment.service.ts +++ b/web/services/issue/issue_comment.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueComment } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueCommentService extends APIService { constructor() { diff --git a/web/services/issue/issue_draft.service.ts b/web/services/issue/issue_draft.service.ts index a93bda776db..3ccd43f5641 100644 --- a/web/services/issue/issue_draft.service.ts +++ b/web/services/issue/issue_draft.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; import { TIssue } from "@plane/types"; export class IssueDraftService extends APIService { diff --git a/web/services/issue_filter.service.ts b/web/services/issue_filter.service.ts index 5103a4bc8c6..664666a3bc9 100644 --- a/web/services/issue_filter.service.ts +++ b/web/services/issue_filter.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IIssueFiltersResponse } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class IssueFiltersService extends APIService { constructor() { diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 1efad8a2394..9942f691ce4 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class ModuleService extends APIService { constructor() { diff --git a/web/services/notification.service.ts b/web/services/notification.service.ts index db9c6d6d10d..d12656c097f 100644 --- a/web/services/notification.service.ts +++ b/web/services/notification.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -9,7 +10,6 @@ import type { IMarkAllAsReadPayload, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class NotificationService extends APIService { constructor() { diff --git a/web/services/project/project-estimate.service.ts b/web/services/project/project-estimate.service.ts index 6d276c7b96b..880c4dd8d82 100644 --- a/web/services/project/project-estimate.service.ts +++ b/web/services/project/project-estimate.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectEstimateService extends APIService { constructor() { diff --git a/web/services/project/project-export.service.ts b/web/services/project/project-export.service.ts index b5503a829f3..cc8cebe719a 100644 --- a/web/services/project/project-export.service.ts +++ b/web/services/project/project-export.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectExportService extends APIService { constructor() { diff --git a/web/services/project/project-state.service.ts b/web/services/project/project-state.service.ts index 9f846987ee7..4087ada30e2 100644 --- a/web/services/project/project-state.service.ts +++ b/web/services/project/project-state.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IState } from "@plane/types"; diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 41111db98c9..691e6c028c1 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -12,7 +13,6 @@ import type { IUserEmailNotificationSettings, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class UserService extends APIService { constructor() { diff --git a/web/services/view.service.ts b/web/services/view.service.ts index 95ae7dd0662..f09eea563cc 100644 --- a/web/services/view.service.ts +++ b/web/services/view.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IProjectView } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ViewService extends APIService { constructor() { diff --git a/web/services/webhook.service.ts b/web/services/webhook.service.ts index abfe7c46d81..d021799fb9b 100644 --- a/web/services/webhook.service.ts +++ b/web/services/webhook.service.ts @@ -1,7 +1,7 @@ // api services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWebhook } from "@plane/types"; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 2515853f590..bfeadad03d7 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWorkspace, diff --git a/web/store/application/app-config.store.ts b/web/store/application/app-config.store.ts index 6faef8b697f..aec22a4ce30 100644 --- a/web/store/application/app-config.store.ts +++ b/web/store/application/app-config.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { AppConfigService } from "services/app_config.service"; import { IAppConfig } from "@plane/types"; // services -import { AppConfigService } from "services/app_config.service"; export interface IAppConfigStore { // observables diff --git a/web/store/application/command-palette.store.ts b/web/store/application/command-palette.store.ts index 22b395e3468..dc10bba88a7 100644 --- a/web/store/application/command-palette.store.ts +++ b/web/store/application/command-palette.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, computed } from "mobx"; // services -import { ProjectService } from "services/project"; -import { PageService } from "services/page.service"; import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { PageService } from "services/page.service"; +import { ProjectService } from "services/project"; export interface ModalData { store: EIssuesStoreType; diff --git a/web/store/application/index.ts b/web/store/application/index.ts index 30333535a42..bad28d4c9fe 100644 --- a/web/store/application/index.ts +++ b/web/store/application/index.ts @@ -1,7 +1,7 @@ import { RootStore } from "store/root.store"; +import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; import { AppConfigStore, IAppConfigStore } from "./app-config.store"; import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; -import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; // import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { InstanceStore, IInstanceStore } from "./instance.store"; import { RouterStore, IRouterStore } from "./router.store"; diff --git a/web/store/application/instance.store.ts b/web/store/application/instance.store.ts index 7c486ef8bfb..b4793fdfb51 100644 --- a/web/store/application/instance.store.ts +++ b/web/store/application/instance.store.ts @@ -1,8 +1,8 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // types +import { InstanceService } from "services/instance.service"; import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration, IInstanceAdmin } from "@plane/types"; // services -import { InstanceService } from "services/instance.service"; export interface IInstanceStore { // issues diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index ee4842539b9..aea87033e6e 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -1,16 +1,16 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import { isFuture, isPast, isToday } from "date-fns"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { ICycle, CycleDateCheckData } from "@plane/types"; // mobx -import { RootStore } from "store/root.store"; // services -import { ProjectService } from "services/project"; -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; +import { ProjectService } from "services/project"; +import { RootStore } from "store/root.store"; +import { ICycle, CycleDateCheckData } from "@plane/types"; export interface ICycleStore { //Loaders diff --git a/web/store/dashboard.store.ts b/web/store/dashboard.store.ts index ad0960c7bb6..c8a07428e01 100644 --- a/web/store/dashboard.store.ts +++ b/web/store/dashboard.store.ts @@ -1,6 +1,6 @@ +import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; // services import { DashboardService } from "services/dashboard.service"; // types diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts index beddd52aba6..9c197ffaa4c 100644 --- a/web/store/estimate.store.ts +++ b/web/store/estimate.store.ts @@ -1,11 +1,11 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { ProjectEstimateService } from "services/project"; // types import { RootStore } from "store/root.store"; import { IEstimate, IEstimateFormData } from "@plane/types"; -import { computedFn } from "mobx-utils"; export interface IEstimateStore { //Loaders diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 744ad44fbab..f117e6cbc20 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -1,7 +1,6 @@ import { action, computed, makeObservable, observable } from "mobx"; import posthog from "posthog-js"; // stores -import { RootStore } from "./root.store"; import { GROUP_WORKSPACE, WORKSPACE_CREATED, @@ -15,6 +14,7 @@ import { getWorkspaceEventPayload, getPageEventPayload, } from "constants/event-tracker"; +import { RootStore } from "./root.store"; export interface IEventTrackerStore { // properties diff --git a/web/store/global-view.store.ts b/web/store/global-view.store.ts index 65aedadb5ab..60d97f6332f 100644 --- a/web/store/global-view.store.ts +++ b/web/store/global-view.store.ts @@ -1,6 +1,6 @@ +import { set } from "lodash"; import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { computedFn } from "mobx-utils"; -import { set } from "lodash"; // services import { WorkspaceService } from "services/workspace.service"; // types diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index 8d8f2bec546..803af3095f9 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -1,9 +1,9 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { observable, action, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services import { InboxService } from "services/inbox/inbox.service"; // types diff --git a/web/store/inbox/inbox_filter.store.ts b/web/store/inbox/inbox_filter.store.ts index c4566acbe26..8bad22cdd40 100644 --- a/web/store/inbox/inbox_filter.store.ts +++ b/web/store/inbox/inbox_filter.store.ts @@ -1,6 +1,6 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services import { InboxService } from "services/inbox.service"; // types diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 4f980357fdb..2ecbedff0a8 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -1,10 +1,10 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { observable, action, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services import { InboxIssueService } from "services/inbox/inbox-issue.service"; // types diff --git a/web/store/inbox/root.store.ts b/web/store/inbox/root.store.ts index b0706cca710..982de47bcd1 100644 --- a/web/store/inbox/root.store.ts +++ b/web/store/inbox/root.store.ts @@ -1,8 +1,8 @@ // types import { RootStore } from "store/root.store"; import { IInbox, Inbox } from "./inbox.store"; -import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; import { IInboxFilter, InboxFilter } from "./inbox_filter.store"; +import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; export interface IInboxRootStore { rootStore: RootStore; diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 032928cda6a..12d541bea24 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IArchivedIssuesFilter { // observables diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index a0b26eb8bce..06aa9d29a32 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueArchiveService } from "services/issue"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IArchivedIssues { // observable diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index 5d8c2a6b86c..c4a345c4752 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface ICycleIssuesFilter { // observables diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 61b280da9ea..ef6e1872d3f 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES"; diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index cc58a775574..51b8d9bc76e 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IDraftIssuesFilter { // observables diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index a06213eb0cd..67dcf2729c9 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -1,16 +1,16 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; -import uniq from "lodash/uniq"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueDraftService } from "services/issue/issue_draft.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IDraftIssues { // observable diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index baac4a2ade7..2921e9ca883 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -1,5 +1,9 @@ import isEmpty from "lodash/isEmpty"; // types +// constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// lib +import { storage } from "lib/local-storage"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, @@ -10,10 +14,6 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; -// constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -// lib -import { storage } from "lib/local-storage"; interface ILocalStoreIssueFilters { key: EIssuesStoreType; diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index a267ac9c81a..235f65e7c71 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -1,16 +1,16 @@ -import orderBy from "lodash/orderBy"; import get from "lodash/get"; import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; import values from "lodash/values"; // types -import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; -import { IIssueRootStore } from "../root.store"; // constants import { ISSUE_PRIORITIES } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; diff --git a/web/store/issue/issue-details/activity.store.ts b/web/store/issue/issue-details/activity.store.ts index efa181c9532..5afb6d8e46b 100644 --- a/web/store/issue/issue-details/activity.store.ts +++ b/web/store/issue/issue-details/activity.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; +import concat from "lodash/concat"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; -import update from "lodash/update"; -import concat from "lodash/concat"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueActivityService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TActivityLoader = "fetch" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/attachment.store.ts b/web/store/issue/issue-details/attachment.store.ts index 5341058c192..47e95b4372d 100644 --- a/web/store/issue/issue-details/attachment.store.ts +++ b/web/store/issue/issue-details/attachment.store.ts @@ -1,14 +1,14 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueAttachmentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueAttachmentStoreActions { addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void; diff --git a/web/store/issue/issue-details/comment.store.ts b/web/store/issue/issue-details/comment.store.ts index 4336971defb..434be2778df 100644 --- a/web/store/issue/issue-details/comment.store.ts +++ b/web/store/issue/issue-details/comment.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueCommentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TCommentLoader = "fetch" | "create" | "update" | "delete" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/comment_reaction.store.ts b/web/store/issue/issue-details/comment_reaction.store.ts index 59adeef621a..832f798d9a1 100644 --- a/web/store/issue/issue-details/comment_reaction.store.ts +++ b/web/store/issue/issue-details/comment_reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueCommentReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index f42c1337657..ba1d9b75273 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,9 +1,9 @@ import { makeObservable } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue"; // types import { TIssue } from "@plane/types"; -import { computedFn } from "mobx-utils"; import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { diff --git a/web/store/issue/issue-details/link.store.ts b/web/store/issue/issue-details/link.store.ts index 81d13438c0e..1cfd47c3f6d 100644 --- a/web/store/issue/issue-details/link.store.ts +++ b/web/store/issue/issue-details/link.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueLinkStoreActions { addLinks: (issueId: string, links: TIssueLink[]) => void; diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index 6282ac40e1f..a32ba6eca6e 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/relation.store.ts b/web/store/issue/issue-details/relation.store.ts index da729540ed8..fafa4ad4d6e 100644 --- a/web/store/issue/issue-details/relation.store.ts +++ b/web/store/issue/issue-details/relation.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueRelationService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelationTypes, TIssueRelation, TIssue } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueRelationStoreActions { // actions diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index db5dab307f4..be77efcd1ed 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -1,20 +1,5 @@ import { action, computed, makeObservable, observable } from "mobx"; // types -import { IIssueRootStore } from "../root.store"; -import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; -import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; -import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; -import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; -import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; -import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; -import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; -import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; -import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; -import { - IIssueCommentReactionStore, - IssueCommentReactionStore, - IIssueCommentReactionStoreActions, -} from "./comment_reaction.store"; import { TIssue, TIssueAttachment, @@ -24,6 +9,21 @@ import { TIssueReaction, TIssueRelationTypes, } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; +import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; +import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; +import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; +import { + IIssueCommentReactionStore, + IssueCommentReactionStore, + IIssueCommentReactionStoreActions, +} from "./comment_reaction.store"; +import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; +import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; +import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; +import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; +import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; +import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; export type TPeekIssue = { workspaceSlug: string; diff --git a/web/store/issue/issue-details/sub_issues.store.ts b/web/store/issue/issue-details/sub_issues.store.ts index cfa1be12e1a..87ec58930f6 100644 --- a/web/store/issue/issue-details/sub_issues.store.ts +++ b/web/store/issue/issue-details/sub_issues.store.ts @@ -1,12 +1,11 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; import concat from "lodash/concat"; -import update from "lodash/update"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssue, TIssueSubIssues, @@ -14,6 +13,7 @@ import { TIssueSubIssuesIdMap, TSubIssuesStateDistribution, } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueSubIssuesStoreActions { fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise; diff --git a/web/store/issue/issue-details/subscription.store.ts b/web/store/issue/issue-details/subscription.store.ts index 276c952f406..48b353e72e4 100644 --- a/web/store/issue/issue-details/subscription.store.ts +++ b/web/store/issue/issue-details/subscription.store.ts @@ -1,5 +1,5 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { NotificationService } from "services/notification.service"; // types diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index cbda505ff2a..635c75b241c 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -1,12 +1,12 @@ -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; // store import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { IssueService } from "services/issue"; import { TIssue } from "@plane/types"; //services -import { IssueService } from "services/issue"; export type IIssueStore = { // observables diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index ac4a608098f..98181d73018 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -1,9 +1,9 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"; // helpers +import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { generateCalendarData } from "helpers/calendar.helper"; // types -import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { getWeekNumberOfDate } from "helpers/date-time.helper"; export interface ICalendarStore { diff --git a/web/store/issue/issue_gantt_view.store.ts b/web/store/issue/issue_gantt_view.store.ts index b087554dd5d..e478e86491a 100644 --- a/web/store/issue/issue_gantt_view.store.ts +++ b/web/store/issue/issue_gantt_view.store.ts @@ -1,9 +1,9 @@ import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // helpers +import { ChartDataType, TGanttViews } from "components/gantt-chart"; import { currentViewDataWithView } from "components/gantt-chart/data"; // types -import { ChartDataType, TGanttViews } from "components/gantt-chart"; export interface IGanttStore { // observables diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index c353059efa6..c34a31b23b9 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IModuleIssuesFilter { // observables diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 9e6ad3f49f4..66847319503 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IModuleIssues { // observable diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 658980082b0..c7ebc378ad6 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProfileIssuesFilter { // observables diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index c39b33a80d1..39a37a2cf96 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { UserService } from "services/user.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { UserService } from "services/user.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; interface IProfileIssueTabTypes { [key: string]: string[]; diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index c7c8988b167..27c98036075 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { ViewService } from "services/view.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { ViewService } from "services/view.service"; export interface IProjectViewIssuesFilter { // observables diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index b85465ec81f..012d6ebe8cd 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService } from "services/issue/issue.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue/issue.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectViewIssues { // observable diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index f18654cde56..d5c35348722 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProjectIssuesFilter { // observables @@ -189,7 +189,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index f3ee9478370..080b8cee6b4 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -1,15 +1,15 @@ -import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import concat from "lodash/concat"; +import pull from "lodash/pull"; import set from "lodash/set"; import update from "lodash/update"; -import pull from "lodash/pull"; -import concat from "lodash/concat"; +import { action, makeObservable, observable, runInAction, computed } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectIssues { // observable diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index def91d200e9..a9dde82ae3e 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -1,28 +1,28 @@ -import { autorun, makeObservable, observable } from "mobx"; import isEmpty from "lodash/isEmpty"; +import { autorun, makeObservable, observable } from "mobx"; // root store +import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; import { RootStore } from "../root.store"; import { IStateStore, StateStore } from "../state.store"; // issues data store -import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; -import { IIssueStore, IssueStore } from "./issue.store"; +import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; +import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; +import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; import { IIssueDetail, IssueDetail } from "./issue-details/root.store"; -import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; +import { IIssueStore, IssueStore } from "./issue.store"; +import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; +import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; +import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProfileIssuesFilter, ProfileIssuesFilter, IProfileIssues, ProfileIssues } from "./profile"; import { IProjectIssuesFilter, ProjectIssuesFilter, IProjectIssues, ProjectIssues } from "./project"; -import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; -import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProjectViewIssuesFilter, ProjectViewIssuesFilter, IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; -import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; -import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; -import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; -import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; export interface IIssueRootStore { currentUserId: string | undefined; diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 76b861f4be7..d6f1aba74d9 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import isArray from "lodash/isArray"; import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; import pickBy from "lodash/pickBy"; -import isArray from "lodash/isArray"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { WorkspaceService } from "services/workspace.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -18,10 +16,12 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { WorkspaceService } from "services/workspace.service"; type TWorkspaceFilters = "all-issues" | "assigned" | "created" | "subscribed" | string; export interface IWorkspaceIssuesFilter { diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index b7fe43b3007..cc859755f76 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -1,14 +1,14 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { WorkspaceService } from "services/workspace.service"; -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IWorkspaceIssues { // observable diff --git a/web/store/label.store.ts b/web/store/label.store.ts index 769ef16a9a3..386676dfe9a 100644 --- a/web/store/label.store.ts +++ b/web/store/label.store.ts @@ -1,11 +1,11 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { buildTree } from "helpers/array.helper"; import { IssueLabelService } from "services/issue"; // helpers -import { buildTree } from "helpers/array.helper"; // types import { RootStore } from "store/root.store"; import { IIssueLabel, IIssueLabelTree } from "@plane/types"; diff --git a/web/store/member/index.ts b/web/store/member/index.ts index a7eba3971d5..d43398d0b1f 100644 --- a/web/store/member/index.ts +++ b/web/store/member/index.ts @@ -2,8 +2,8 @@ import { action, makeObservable, observable } from "mobx"; // types import { RootStore } from "store/root.store"; import { IUserLite } from "@plane/types"; -import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store"; +import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; export interface IMemberRootStore { // observables diff --git a/web/store/member/project-member.store.ts b/web/store/member/project-member.store.ts index 71e2e2dcdd0..6cb39e2efad 100644 --- a/web/store/member/project-member.store.ts +++ b/web/store/member/project-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserProjectRoles } from "constants/project"; import { ProjectMemberService } from "services/project"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; import { IMemberRootStore } from "."; -import { IRouterStore } from "store/application/router.store"; -import { IUserRootStore } from "store/user"; interface IProjectMemberDetails { id: string; diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 4a696bfd289..a901dccc1c2 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserWorkspaceRoles } from "constants/workspace"; import { WorkspaceService } from "services/workspace.service"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { IRouterStore } from "store/application/router.store"; import { IMemberRootStore } from "."; -import { IUserRootStore } from "store/user"; export interface IWorkspaceMembership { id: string; diff --git a/web/store/mention.store.ts b/web/store/mention.store.ts index 872efeb4122..5cfa0478a3b 100644 --- a/web/store/mention.store.ts +++ b/web/store/mention.store.ts @@ -1,6 +1,6 @@ +import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; import { computed, makeObservable } from "mobx"; // editor -import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; // types import { RootStore } from "store/root.store"; diff --git a/web/store/module.store.ts b/web/store/module.store.ts index 2b4522cd09a..c7dcba79c06 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -1,13 +1,13 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services -import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; +import { ProjectService } from "services/project"; // types -import { IModule, ILinkDetails } from "@plane/types"; import { RootStore } from "store/root.store"; +import { IModule, ILinkDetails } from "@plane/types"; export interface IModuleStore { //Loaders diff --git a/web/store/page.store.ts b/web/store/page.store.ts index fa5970e49ae..ae416237f37 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable, reaction, runInAction } from "mobx"; -import { IIssueLabel, IPage } from "@plane/types"; import { PageService } from "services/page.service"; +import { IIssueLabel, IPage } from "@plane/types"; import { RootStore } from "./root.store"; diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index 072605bc34f..c16e8ab0887 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -1,5 +1,6 @@ -import { makeObservable, observable, runInAction, action, computed } from "mobx"; +import { isThisWeek, isToday, isYesterday } from "date-fns"; import { set } from "lodash"; +import { makeObservable, observable, runInAction, action, computed } from "mobx"; // services import { PageService } from "services/page.service"; // store @@ -7,7 +8,6 @@ import { PageStore, IPageStore } from "store/page.store"; // types import { IPage, IRecentPages } from "@plane/types"; import { RootStore } from "./root.store"; -import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { loader: boolean; diff --git a/web/store/project/index.ts b/web/store/project/index.ts index 696b3c802aa..dff0db175a9 100644 --- a/web/store/project/index.ts +++ b/web/store/project/index.ts @@ -1,6 +1,6 @@ -import { IProjectStore, ProjectStore } from "./project.store"; -import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; import { RootStore } from "store/root.store"; +import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; +import { IProjectStore, ProjectStore } from "./project.store"; export interface IProjectRootStore { project: IProjectStore; diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index 3a94b861147..9be1cb48c75 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -1,9 +1,9 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { ProjectPublishService } from "services/project"; import { ProjectRootStore } from "./"; // services -import { ProjectPublishService } from "services/project"; export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 176c3a3649f..1b9220a2da7 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -1,14 +1,14 @@ -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; +import { cloneDeep, update } from "lodash"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { RootStore } from "../root.store"; -import { IProject } from "@plane/types"; -// services import { IssueLabelService, IssueService } from "services/issue"; import { ProjectService, ProjectStateService } from "services/project"; -import { cloneDeep, update } from "lodash"; +import { IProject } from "@plane/types"; +import { RootStore } from "../root.store"; +// services export interface IProjectStore { // observables searchQuery: string; diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 3e07332499b..298cd532e78 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -1,23 +1,23 @@ import { enableStaticRendering } from "mobx-react-lite"; // root stores import { AppRootStore, IAppRootStore } from "./application"; +import { CycleStore, ICycleStore } from "./cycle.store"; +import { DashboardStore, IDashboardStore } from "./dashboard.store"; +import { IEstimateStore, EstimateStore } from "./estimate.store"; import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; +import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; +import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; +import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; +import { ILabelStore, LabelStore } from "./label.store"; +import { IMemberRootStore, MemberRootStore } from "./member"; +import { IMentionStore, MentionStore } from "./mention.store"; +import { IModuleStore, ModulesStore } from "./module.store"; import { IProjectRootStore, ProjectRootStore } from "./project"; -import { CycleStore, ICycleStore } from "./cycle.store"; import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; -import { IModuleStore, ModulesStore } from "./module.store"; +import { IStateStore, StateStore } from "./state.store"; import { IUserRootStore, UserRootStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; -import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; -import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; -import { IStateStore, StateStore } from "./state.store"; -import { IMemberRootStore, MemberRootStore } from "./member"; -import { IEstimateStore, EstimateStore } from "./estimate.store"; -import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; -import { IMentionStore, MentionStore } from "./mention.store"; -import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { IProjectPageStore, ProjectPageStore } from "./project-page.store"; -import { ILabelStore, LabelStore } from "./label.store"; enableStaticRendering(typeof window === "undefined"); diff --git a/web/store/state.store.ts b/web/store/state.store.ts index 783a82ee27c..df3496f394c 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -1,15 +1,15 @@ -import { makeObservable, observable, computed, action, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import groupBy from "lodash/groupBy"; import set from "lodash/set"; +import { makeObservable, observable, computed, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // store +import { sortStates } from "helpers/state.helper"; +import { ProjectStateService } from "services/project"; +import { IState } from "@plane/types"; import { RootStore } from "./root.store"; // types -import { IState } from "@plane/types"; // services -import { ProjectStateService } from "services/project"; // helpers -import { sortStates } from "helpers/state.helper"; export interface IStateStore { //Loaders diff --git a/web/store/user/index.ts b/web/store/user/index.ts index ada2e6be7b9..1a94e16b39f 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -1,7 +1,7 @@ import { action, observable, runInAction, makeObservable } from "mobx"; // services -import { UserService } from "services/user.service"; import { AuthService } from "services/auth.service"; +import { UserService } from "services/user.service"; // interfaces import { IUser, IUserSettings } from "@plane/types"; // store diff --git a/web/store/user/user-membership.store.ts b/web/store/user/user-membership.store.ts index b8bdbfac505..a1f5c1b8161 100644 --- a/web/store/user/user-membership.store.ts +++ b/web/store/user/user-membership.store.ts @@ -1,6 +1,8 @@ -import { action, observable, runInAction, makeObservable, computed } from "mobx"; import { set } from "lodash"; +import { action, observable, runInAction, makeObservable, computed } from "mobx"; // services +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { ProjectMemberService } from "services/project"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; @@ -8,8 +10,6 @@ import { WorkspaceService } from "services/workspace.service"; import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types"; import { RootStore } from "../root.store"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; export interface IUserMembershipStore { // observables diff --git a/web/store/workspace/api-token.store.ts b/web/store/workspace/api-token.store.ts index f0772933dff..351ead5610c 100644 --- a/web/store/workspace/api-token.store.ts +++ b/web/store/workspace/api-token.store.ts @@ -2,9 +2,9 @@ import { action, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { APITokenService } from "services/api_token.service"; +import { IApiToken } from "@plane/types"; import { RootStore } from "../root.store"; // types -import { IApiToken } from "@plane/types"; export interface IApiTokenStore { // observables diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 4020aaef711..863982e1a98 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,13 +1,13 @@ +import set from "lodash/set"; import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { WorkspaceService } from "services/workspace.service"; +import { IWorkspace } from "@plane/types"; import { RootStore } from "../root.store"; -import set from "lodash/set"; // types -import { IWorkspace } from "@plane/types"; // services -import { WorkspaceService } from "services/workspace.service"; // sub-stores -import { IWebhookStore, WebhookStore } from "./webhook.store"; import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; +import { IWebhookStore, WebhookStore } from "./webhook.store"; export interface IWorkspaceRootStore { // observables diff --git a/web/store/workspace/webhook.store.ts b/web/store/workspace/webhook.store.ts index 5657f341e17..256b41e388d 100644 --- a/web/store/workspace/webhook.store.ts +++ b/web/store/workspace/webhook.store.ts @@ -1,8 +1,8 @@ // mobx import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { IWebhook } from "@plane/types"; import { WebhookService } from "services/webhook.service"; +import { IWebhook } from "@plane/types"; import { RootStore } from "../root.store"; export interface IWebhookStore { diff --git a/yarn.lock b/yarn.lock index 0a21fcee2ff..9518afff6f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,13 +29,6 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -269,7 +262,7 @@ "@babel/traverse" "^7.23.6" "@babel/types" "^7.23.6" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.23.4": +"@babel/highlight@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== @@ -1288,37 +1281,7 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== - dependencies: - ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.4": +"@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== @@ -1333,15 +1296,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.36.0": - version "8.36.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" - integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== - -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@floating-ui/core@^1.4.2": version "1.5.2" @@ -1399,38 +1357,24 @@ redux "^4.2.1" use-memo-one "^1.1.3" -"@humanwhocodes/config-array@^0.11.13", "@humanwhocodes/config-array@^0.11.8": - version "0.11.13" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" - integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^2.0.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@humanwhocodes/object-schema@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" - integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== "@hypnosphi/create-react-context@^0.3.1": version "0.3.1" @@ -1591,33 +1535,12 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== -"@next/eslint-plugin-next@12.2.2": - version "12.2.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.2.2.tgz#b4a22c06b6454068b54cc44502168d90fbb29a6d" - integrity sha512-XOi0WzJhGH3Lk51SkSu9eZxF+IY1ZZhWcJTIGBycAbWU877IQa6+6KxMATWCOs7c+bmp6Sd8KywXJaDRxzu0JA== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.0.tgz#cf3d799b21671554c1f5889c01d2513afb9973cd" - integrity sha512-z+gnX4Zizatqatc6f4CQrcC9oN8Us3Vrq/OLyc98h7K/eWctrnV91zFZodmJHUjx0cITY8uYM7LXD7IdYkg3kg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.1": - version "13.2.1" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.1.tgz#58dea4d53c0adfc59c10195f51eb8d3575fce414" - integrity sha512-r0i5rcO6SMAZtqiGarUVMr3k256X0R0j6pEkKg4PxqUW+hG0qgMxRVAJsuoRG5OBFkCOlSfWZJ0mP9fQdCcyNg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz#3e124cd10ce24dab5d3448ce04104b4f1f4c6ca7" - integrity sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ== +"@next/eslint-plugin-next@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz#29b041233fac7417e22eefa4146432d5cd910820" + integrity sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q== dependencies: - glob "7.1.7" + glob "10.3.10" "@next/swc-darwin-arm64@14.0.4": version "14.0.4" @@ -2199,10 +2122,10 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262" integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA== -"@rushstack/eslint-patch@^1.1.3": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz#9ab8f811930d7af3e3d549183a50884f9eb83f36" - integrity sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw== +"@rushstack/eslint-patch@^1.3.3": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" + integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== "@scena/dragscroll@^1.4.0": version "1.4.0" @@ -2799,7 +2722,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": +"@types/react@*", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== @@ -2883,16 +2806,16 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/eslint-plugin@^6.13.2": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz#cc29fbd208ea976de3db7feb07755bba0ce8d8bc" - integrity sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ== +"@typescript-eslint/eslint-plugin@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz#dd71fc5c7ecec745ca26ece506d84d203a205c0e" + integrity sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/type-utils" "6.16.0" - "@typescript-eslint/utils" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/type-utils" "7.1.1" + "@typescript-eslint/utils" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -2900,14 +2823,26 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.21.0", "@typescript-eslint/parser@^5.42.0", "@typescript-eslint/parser@^5.48.2": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/parser@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.1.1.tgz#6a9d0a5c9ccdf5dbd3cb8c949728c64e24e07d1f" + integrity sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ== + dependencies: + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -2918,13 +2853,21 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz#f3e9a00fbc1d0701356359cd56489c54d9e37168" - integrity sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/scope-manager@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz#9e301803ff8e21a74f50c6f89a4baccad9a48f93" + integrity sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -2936,13 +2879,13 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz#5f21c3e49e540ad132dc87fc99af463c184d5ed1" - integrity sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg== +"@typescript-eslint/type-utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz#aee820d5bedd39b83c18585a526cc520ddb7a226" + integrity sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g== dependencies: - "@typescript-eslint/typescript-estree" "6.16.0" - "@typescript-eslint/utils" "6.16.0" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/utils" "7.1.1" debug "^4.3.4" ts-api-utils "^1.0.1" @@ -2951,10 +2894,15 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.16.0.tgz#a3abe0045737d44d8234708d5ed8fef5d59dc91e" - integrity sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/types@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.1.1.tgz#ca33ba7cf58224fb46a84fea62593c2c53cd795f" + integrity sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -2969,13 +2917,27 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz#d6e0578e4f593045f0df06c4b3a22bd6f13f2d03" - integrity sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/typescript-estree@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz#09c54af0151a1b05d0875c0fc7fe2ec7a2476ece" + integrity sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -2997,17 +2959,17 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.16.0.tgz#1c291492d34670f9210d2b7fcf6b402bea3134ae" - integrity sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A== +"@typescript-eslint/utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.1.1.tgz#bdeeb789eee4af5d3fb5400a69566d4dbf97ff3b" + integrity sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/typescript-estree" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" semver "^7.5.4" "@typescript-eslint/visitor-keys@5.62.0": @@ -3018,12 +2980,20 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz#d50da18a05d91318ed3e7e8889bda0edc35f3a10" - integrity sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@typescript-eslint/visitor-keys@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz#e6538a58c9b157f03bcbb29e3b6a92fe39a6ab0d" + integrity sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ== dependencies: - "@typescript-eslint/types" "6.16.0" + "@typescript-eslint/types" "7.1.1" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -3031,16 +3001,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - acorn@^8.8.2, acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -3058,7 +3023,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3068,7 +3033,7 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.6.0: +ajv@^8.6.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3078,11 +3043,6 @@ ajv@^8.0.1, ajv@^8.6.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3130,13 +3090,6 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -3164,7 +3117,7 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -3213,7 +3166,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -3252,11 +3205,6 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async@^3.2.3: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -3909,7 +3857,7 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4146,14 +4094,6 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -4432,77 +4372,32 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@12.2.2: - version "12.2.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.2.2.tgz#4bb996026e118071849bc4011283a160ad5bde46" - integrity sha512-oJhWBLC4wDYYUFv/5APbjHUFd0QRFCojMdj/QnMoOEktmeTvwnnoA8F8uaXs0fQgsaTK0tbUxBRv9/Y4/rpxOA== +eslint-config-next@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.0.tgz#7e309d426b8afacaba3b32fdbb02ba220b6d0a97" + integrity sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg== dependencies: - "@next/eslint-plugin-next" "12.2.2" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.29.4" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.0.0.tgz#d533ee1dbd6576fd3759ba4db4d5a6c4e039c242" - integrity sha512-y2nqWS2tycWySdVhb+rhp6CuDmDazGySqkzzQZf3UTyfHyC7og1m5m/AtMFwCo5mtvDqvw1BENin52kV9733lg== - dependencies: - "@next/eslint-plugin-next" "13.0.0" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.2.1: - version "13.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.1.tgz#644fb3496b832bc1e32f2c57cce1ec3eeb7bb7a1" - integrity sha512-2GAx7EjSiCzJN6H2L/v1kbYrNiwQxzkyjy6eWSjuhAKt+P6d3nVNHGy9mON8ZcYd72w/M8kyMjm4UB9cvijgrw== - dependencies: - "@next/eslint-plugin-next" "13.2.1" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.4.tgz#8aa4d42da3a575a814634ba9c88c8d25266c5fdd" - integrity sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg== - dependencies: - "@next/eslint-plugin-next" "13.2.4" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@next/eslint-plugin-next" "14.1.0" + "@rushstack/eslint-patch" "^1.3.3" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-import "^2.28.1" + eslint-plugin-jsx-a11y "^6.7.1" + eslint-plugin-react "^7.33.2" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" -eslint-config-prettier@^8.3.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" - integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== -eslint-config-turbo@latest: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.11.2.tgz#8e6c456f58e88ecc9adface9c5e03fa782e8bba5" - integrity sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg== +eslint-config-turbo@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.12.4.tgz#b911aced2228e98176dbebe0f1ebef345a253400" + integrity sha512-5hqEaV6PNmAYLL4RTmq74OcCt8pgzOLnfDVPG/7PUXpQ0Mpz0gr926oCSFukywKKXjdum3VHD84S7Z9A/DqTAw== dependencies: - eslint-plugin-turbo "1.11.2" + eslint-plugin-turbo "1.12.4" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -4513,17 +4408,6 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz#a90a4a1c80da8d632df25994c4c5fdcdd02b8751" - integrity sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ== - dependencies: - debug "^4.3.4" - glob "^7.2.0" - is-glob "^4.0.3" - resolve "^1.22.0" - tsconfig-paths "^3.14.1" - eslint-import-resolver-typescript@^3.5.2: version "3.6.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" @@ -4544,7 +4428,7 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: +eslint-plugin-import@^2.28.1, eslint-plugin-import@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== @@ -4567,7 +4451,7 @@ eslint-plugin-import@^2.26.0: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.5.1: +eslint-plugin-jsx-a11y@^6.7.1: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== @@ -4589,32 +4473,12 @@ eslint-plugin-jsx-a11y@^6.5.1: object.entries "^1.1.7" object.fromentries "^2.0.7" -eslint-plugin-react-hooks@^4.5.0: +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version "4.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@7.31.8: - version "7.31.8" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf" - integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== - dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" - prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.7" - -eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: +eslint-plugin-react@^7.33.2: version "7.33.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== @@ -4636,10 +4500,10 @@ eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: semver "^6.3.1" string.prototype.matchall "^4.0.8" -eslint-plugin-turbo@1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.11.2.tgz#7bb450cced51d35369a678114c2ee9882937adc5" - integrity sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw== +eslint-plugin-turbo@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.12.4.tgz#f29ddd89cb853db5dd4332db39ec2d85c713041e" + integrity sha512-3AGmXvH7E4i/XTWqBrcgu+G7YKZJV/8FrEn79kTd50ilNsv+U3nS2IlcCrQB6Xm2m9avGD9cadLzKDR1/UF2+g== dependencies: dotenv "16.0.3" @@ -4651,7 +4515,7 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1, eslint-scope@^7.2.2: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -4659,182 +4523,21 @@ eslint-scope@^7.1.1, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.34.0: - version "8.34.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" - integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@8.36.0: - version "8.36.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" - integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.1" - "@eslint/js" "8.36.0" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - espree "^9.5.0" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@^7.23.0, eslint@^7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -eslint@^8.31.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -4869,16 +4572,7 @@ eslint@^8.31.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - -espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4887,12 +4581,7 @@ espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0, esquery@^1.4.2: +esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -5162,11 +4851,6 @@ function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -5249,19 +4933,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^10.3.10: +glob@10.3.10, glob@^10.3.10: version "10.3.10" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== @@ -5272,7 +4944,7 @@ glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.3, glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5300,7 +4972,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0, globals@^13.6.0, globals@^13.9.0: +globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== @@ -5349,11 +5021,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -5463,11 +5130,6 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -5478,7 +5140,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5877,24 +5539,11 @@ js-cookie@^3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -js-sdsl@^4.1.4: - version "4.4.2" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" - integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -6143,11 +5792,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -6577,7 +6221,7 @@ minimatch@9.0.3, minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6819,7 +6463,7 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: +object.entries@^1.1.6, object.entries@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== @@ -6828,7 +6472,7 @@ object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.5, object.fromentries@^2.0.6, object.fromentries@^2.0.7: +object.fromentries@^2.0.6, object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -6847,7 +6491,7 @@ object.groupby@^1.0.1: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.hasown@^1.1.1, object.hasown@^1.1.2: +object.hasown@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== @@ -6869,7 +6513,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== @@ -6892,7 +6536,7 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -optionator@^0.9.1, optionator@^0.9.3: +optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -7221,7 +6865,7 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -7724,11 +7368,6 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" -regexpp@^3.1.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -7787,7 +7426,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.2, resolve@^1.22.4: +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7796,7 +7435,7 @@ resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: +resolve@^2.0.0-next.4: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== @@ -7952,12 +7591,12 @@ selecto@~1.26.3: keycon "^1.2.0" overlap-area "^1.1.0" -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8077,15 +7716,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -8144,11 +7774,6 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - stacktrace-parser@^0.1.10: version "0.1.10" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" @@ -8169,7 +7794,8 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8187,7 +7813,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== @@ -8246,6 +7872,7 @@ stringify-object@^3.3.0: is-regexp "^1.0.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8274,7 +7901,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -8355,17 +7982,6 @@ tabbable@^6.0.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tailwind-merge@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b" @@ -8591,7 +8207,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tsconfig-paths@^3.14.1, tsconfig-paths@^3.15.0: +tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -8788,11 +8404,16 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== -typescript@4.9.5, typescript@^4.7.4: +typescript@4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -8993,11 +8614,6 @@ uvu@^0.5.0: kleur "^4.0.3" sade "^1.7.3" -v8-compile-cache@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" From cace132a2a670fef0c3acbc5543475587b5ff007 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:43:19 +0530 Subject: [PATCH 019/214] [WEB-372] fix: horizontal rule extension now always divider adds below nodes (#3890) * fix: horizontal rule extension now always divider adds below nodes * chore: removing duplicate horizontal rule extension --- .../horizontal-rule/horizontal-rule.ts | 111 ++++++++++++++++++ .../editor/core/src/ui/extensions/index.tsx | 9 +- 2 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts new file mode 100644 index 00000000000..2af845b7a08 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts @@ -0,0 +1,111 @@ +import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core"; +import { NodeSelection, TextSelection } from "@tiptap/pm/state"; + +export interface HorizontalRuleOptions { + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + horizontalRule: { + /** + * Add a horizontal rule + */ + setHorizontalRule: () => ReturnType; + }; + } +} + +export const CustomHorizontalRule = Node.create({ + name: "horizontalRule", + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + group: "block", + + parseHTML() { + return [{ tag: "hr" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["hr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, + + addCommands() { + return { + setHorizontalRule: + () => + ({ chain, state }) => { + const { selection } = state; + const { $from: $originFrom, $to: $originTo } = selection; + + const currentChain = chain(); + + if ($originFrom.parentOffset === 0) { + currentChain.insertContentAt( + { + from: Math.max($originFrom.pos - 1, 0), + to: $originTo.pos, + }, + { + type: this.name, + } + ); + } else if (isNodeSelection(selection)) { + currentChain.insertContentAt($originTo.pos, { + type: this.name, + }); + } else { + currentChain.insertContent({ type: this.name }); + } + + return ( + currentChain + // set cursor after horizontal rule + .command(({ tr, dispatch }) => { + if (dispatch) { + const { $to } = tr.selection; + const posAfter = $to.end(); + + if ($to.nodeAfter) { + if ($to.nodeAfter.isTextblock) { + tr.setSelection(TextSelection.create(tr.doc, $to.pos + 1)); + } else if ($to.nodeAfter.isBlock) { + tr.setSelection(NodeSelection.create(tr.doc, $to.pos)); + } else { + tr.setSelection(TextSelection.create(tr.doc, $to.pos)); + } + } else { + // add node after horizontal rule if it’s the end of the document + const node = $to.parent.type.contentMatch.defaultType?.create(); + + if (node) { + tr.insert(posAfter, node); + tr.setSelection(TextSelection.create(tr.doc, posAfter + 1)); + } + } + + tr.scrollIntoView(); + } + + return true; + }) + .run() + ); + }, + }; + }, + + addInputRules() { + return [ + nodeInputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + type: this.type, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 190731fe0b6..7da381e98a6 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -27,6 +27,7 @@ import { RestoreImage } from "src/types/restore-image"; import { CustomLinkExtension } from "src/ui/extensions/custom-link"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; +import { CustomHorizontalRule } from "./horizontal-rule/horizontal-rule"; export const CoreEditorExtensions = ( mentionConfig: { @@ -55,9 +56,7 @@ export const CoreEditorExtensions = ( }, code: false, codeBlock: false, - horizontalRule: { - HTMLAttributes: { class: "mt-4 mb-4" }, - }, + horizontalRule: false, blockquote: false, dropcursor: { color: "rgba(var(--color-text-100))", @@ -67,6 +66,10 @@ export const CoreEditorExtensions = ( CustomQuoteExtension.configure({ HTMLAttributes: { className: "border-l-4 border-custom-border-300" }, }), + + CustomHorizontalRule.configure({ + HTMLAttributes: { class: "mt-4 mb-4" }, + }), CustomKeymap, ListKeymap, CustomLinkExtension.configure({ From b3d3c0fb06c32421ba6378ffb944f9c9f21ae940 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:46:36 +0530 Subject: [PATCH 020/214] [WEB-654] fix: enums export in the types package (#3887) * fix: enums export in the types package * chore: remove NestedKeyOf type --- .../dashboard.d.ts => dashboard.ts} | 18 +++++++++---- packages/types/src/dashboard/enums.ts | 8 ------ packages/types/src/dashboard/index.ts | 2 -- packages/types/src/index.d.ts | 10 -------- .../dashboard/widgets/assigned-issues.tsx | 10 ++++---- .../dashboard/widgets/created-issues.tsx | 10 ++++---- .../widgets/dropdowns/duration-filter.tsx | 3 +-- .../widgets/issue-panels/tabs-list.tsx | 4 +-- .../dashboard/widgets/issues-by-priority.tsx | 5 +++- .../widgets/issues-by-state-group.tsx | 25 ++++++++----------- web/constants/dashboard.ts | 11 +++++++- web/helpers/dashboard.helper.ts | 4 +-- 12 files changed, 52 insertions(+), 58 deletions(-) rename packages/types/src/{dashboard/dashboard.d.ts => dashboard.ts} (91%) delete mode 100644 packages/types/src/dashboard/enums.ts delete mode 100644 packages/types/src/dashboard/index.ts diff --git a/packages/types/src/dashboard/dashboard.d.ts b/packages/types/src/dashboard.ts similarity index 91% rename from packages/types/src/dashboard/dashboard.d.ts rename to packages/types/src/dashboard.ts index d565f668867..be7d7b3be70 100644 --- a/packages/types/src/dashboard/dashboard.d.ts +++ b/packages/types/src/dashboard.ts @@ -1,8 +1,16 @@ -import { IIssueActivity, TIssuePriorities } from "../issues"; -import { TIssue } from "../issues/issue"; -import { TIssueRelationTypes } from "../issues/issue_relation"; -import { TStateGroups } from "../state"; -import { EDurationFilters } from "./enums"; +import { IIssueActivity, TIssuePriorities } from "./issues"; +import { TIssue } from "./issues/issue"; +import { TIssueRelationTypes } from "./issues/issue_relation"; +import { TStateGroups } from "./state"; + +enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} export type TWidgetKeys = | "overview_stats" diff --git a/packages/types/src/dashboard/enums.ts b/packages/types/src/dashboard/enums.ts deleted file mode 100644 index 2c9efd5c35d..00000000000 --- a/packages/types/src/dashboard/enums.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum EDurationFilters { - NONE = "none", - TODAY = "today", - THIS_WEEK = "this_week", - THIS_MONTH = "this_month", - THIS_YEAR = "this_year", - CUSTOM = "custom", -} diff --git a/packages/types/src/dashboard/index.ts b/packages/types/src/dashboard/index.ts deleted file mode 100644 index dec14aea6a9..00000000000 --- a/packages/types/src/dashboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./dashboard"; -export * from "./enums"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b1eb38a567f..bfebd92d044 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -29,13 +29,3 @@ export * from "./auth"; export * from "./api_token"; export * from "./instance"; export * from "./app"; - -export * from "./enums"; - -export type NestedKeyOf = { - [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object - ? ObjectType[Key] extends { pop: any; push: any } - ? `${Key}` - : `${Key}` | `${Key}.${NestedKeyOf}` - : `${Key}`; -}[keyof ObjectType & (string | number)]; diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 3833d319c19..1e031cacd15 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks +import { useDashboard } from "hooks/store"; +// components import { DurationFilterDropdown, TabsList, @@ -10,14 +12,12 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; -import { useDashboard } from "hooks/store"; -// components // helpers +import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 61a1181e9cb..d36260f2135 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks +import { useDashboard } from "hooks/store"; +// components import { DurationFilterDropdown, TabsList, @@ -10,14 +12,12 @@ import { WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; -import { useDashboard } from "hooks/store"; -// components // helpers +import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper"; // types -import { EDurationFilters, TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "created_issues"; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index 3cf22c350e0..feef7ceca68 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -6,9 +6,8 @@ import { DateFilterModal } from "components/core"; // ui // helpers import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; -// types -import { EDurationFilters } from "@plane/types"; // constants +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; type Props = { customDates?: string[]; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index d5fcea69711..257f73851ae 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // helpers -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; import { cn } from "helpers/common.helper"; // types -import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; // constants +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { durationFilter: EDurationFilters; diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index a8a8f64e8c6..becf322857c 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks +import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, @@ -11,10 +12,12 @@ import { WidgetProps, } from "components/dashboard/widgets"; // helpers +import { getCustomDates } from "helpers/dashboard.helper"; // types +import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants import { IssuesByPriorityGraph } from "components/graphs"; -import { EDurationFilters, TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +import { EDurationFilters } from "constants/dashboard"; const WIDGET_KEY = "issues_by_priority"; diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index 6ffeda0c48d..6857f7ef369 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -10,20 +10,15 @@ import { WidgetProps, } from "components/dashboard/widgets"; import { PieGraph } from "components/ui"; -import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; import { STATE_GROUPS } from "constants/state"; import { getCustomDates } from "helpers/dashboard.helper"; import { useDashboard } from "hooks/store"; // components // helpers // types -import { - EDurationFilters, - TIssuesByStateGroupsWidgetFilters, - TIssuesByStateGroupsWidgetResponse, - TStateGroups, -} from "@plane/types"; +import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; // constants +import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; const WIDGET_KEY = "issues_by_state_groups"; @@ -84,14 +79,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index a3f5f7e0028..35599d661c8 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -10,7 +10,7 @@ import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-i import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types -import { EDurationFilters, TIssuesListTypes, TStateGroups } from "@plane/types"; +import { TIssuesListTypes, TStateGroups } from "@plane/types"; import { Props } from "components/icons/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; @@ -117,6 +117,15 @@ export const STATE_GROUP_GRAPH_COLORS: Record = { cancelled: "#E5484D", }; +export enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} + // filter duration options export const DURATION_FILTER_OPTIONS: { key: EDurationFilters; diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index a61ec7f782a..c8c2e7746e7 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -2,9 +2,9 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYea // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; // types -import { EDurationFilters, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; // constants -import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; /** * @description returns date range based on the duration filter From e4f48d687830621cdebbbbbe83abaa93a14cdc25 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:15:48 +0530 Subject: [PATCH 021/214] [WEB-393] feat: new emoji picker using `emoji-picker-react` (#3868) * chore: emoji-picker-react package added * chore: emoji and emoji picker component added * chore: emoji picker custom style added * chore: migration of the emoji's * chore: migration changes * chore: project logo prop * chore: added logo props in the serializer * chore: removed unused keys * chore: implement emoji picker throughout the web app * style: emoji icon picker * chore: update project logo renderer in the space app * chore: migrations fixes --------- Co-authored-by: Anmol Singh Bhatia Co-authored-by: NarayanBavisetti --- apiserver/plane/app/serializers/project.py | 3 +- apiserver/plane/app/views/workspace.py | 4 - .../db/migrations/0061_alter_issuelink_url.py | 18 - .../db/migrations/0061_project_logo_props.py | 54 + apiserver/plane/db/models/project.py | 1 + packages/types/src/projects.d.ts | 27 +- packages/types/src/users.d.ts | 4 - packages/ui/package.json | 1 + packages/ui/src/emoji/emoji-icon-picker.tsx | 169 +++ packages/ui/src/emoji/icons-list.tsx | 110 ++ packages/ui/src/emoji/icons.ts | 605 +++++++++ packages/ui/src/emoji/index.ts | 1 + packages/ui/src/form-fields/input.tsx | 27 +- packages/ui/src/index.ts | 1 + space/components/common/index.ts | 1 + space/components/common/project-logo.tsx | 34 + space/components/issues/navbar/index.tsx | 47 +- space/types/project.ts | 6 +- .../sidebar/projects-list.tsx | 14 +- .../sidebar/sidebar-header.tsx | 15 +- .../dashboard/widgets/recent-projects.tsx | 16 +- web/components/dropdowns/project.tsx | 24 +- web/components/emoji-icon-picker/emojis.json | 1090 ----------------- web/components/emoji-icon-picker/helpers.ts | 26 - web/components/emoji-icon-picker/icons.json | 607 --------- web/components/emoji-icon-picker/index.tsx | 204 --- web/components/emoji-icon-picker/types.d.ts | 15 - web/components/headers/cycle-issues.tsx | 16 +- web/components/headers/cycles.tsx | 16 +- web/components/headers/module-issues.tsx | 54 +- web/components/headers/modules-list.tsx | 13 +- web/components/headers/page-details.tsx | 12 +- web/components/headers/pages.tsx | 12 +- .../project-archived-issue-details.tsx | 13 +- .../headers/project-archived-issues.tsx | 12 +- .../headers/project-draft-issues.tsx | 12 +- web/components/headers/project-inbox.tsx | 12 +- .../headers/project-issue-details.tsx | 12 +- web/components/headers/project-issues.tsx | 18 +- web/components/headers/project-settings.tsx | 12 +- .../headers/project-view-issues.tsx | 16 +- web/components/headers/project-views.tsx | 16 +- .../filters/applied-filters/project.tsx | 16 +- .../filters/header/filters/project.tsx | 19 +- web/components/issues/issue-layouts/utils.tsx | 9 +- web/components/profile/sidebar.tsx | 30 +- web/components/project/card.tsx | 14 +- .../project/create-project-modal.tsx | 249 ++-- web/components/project/form.tsx | 71 +- web/components/project/index.ts | 1 + web/components/project/project-logo.tsx | 34 + web/components/project/sidebar-list-item.tsx | 86 +- web/helpers/emoji.helper.tsx | 9 + web/helpers/project.helper.ts | 3 + .../settings-layout/project/sidebar.tsx | 4 +- web/pages/_app.tsx | 1 + web/styles/emoji.css | 52 + yarn.lock | 7 +- 58 files changed, 1513 insertions(+), 2462 deletions(-) delete mode 100644 apiserver/plane/db/migrations/0061_alter_issuelink_url.py create mode 100644 apiserver/plane/db/migrations/0061_project_logo_props.py create mode 100644 packages/ui/src/emoji/emoji-icon-picker.tsx create mode 100644 packages/ui/src/emoji/icons-list.tsx create mode 100644 packages/ui/src/emoji/icons.ts create mode 100644 packages/ui/src/emoji/index.ts create mode 100644 space/components/common/project-logo.tsx delete mode 100644 web/components/emoji-icon-picker/emojis.json delete mode 100644 web/components/emoji-icon-picker/helpers.ts delete mode 100644 web/components/emoji-icon-picker/icons.json delete mode 100644 web/components/emoji-icon-picker/index.tsx delete mode 100644 web/components/emoji-icon-picker/types.d.ts create mode 100644 web/components/project/project-logo.tsx create mode 100644 web/styles/emoji.css diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 999233442a4..6840fa8f79a 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -95,8 +95,7 @@ class Meta: "identifier", "name", "cover_image", - "icon_prop", - "emoji", + "logo_props", "description", ] read_only_fields = fields diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 84ba125bac1..7c4a5db8d75 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1366,10 +1366,6 @@ def get(self, request, slug, user_id): ) .values( "id", - "name", - "identifier", - "emoji", - "icon_prop", "created_issues", "assigned_issues", "completed_issues", diff --git a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py b/apiserver/plane/db/migrations/0061_alter_issuelink_url.py deleted file mode 100644 index 1aca84a8000..00000000000 --- a/apiserver/plane/db/migrations/0061_alter_issuelink_url.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2024-03-01 07:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0060_cycle_progress_snapshot'), - ] - - operations = [ - migrations.AlterField( - model_name='issuelink', - name='url', - field=models.TextField(), - ), - ] diff --git a/apiserver/plane/db/migrations/0061_project_logo_props.py b/apiserver/plane/db/migrations/0061_project_logo_props.py new file mode 100644 index 00000000000..d8752d9dd8f --- /dev/null +++ b/apiserver/plane/db/migrations/0061_project_logo_props.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.7 on 2024-03-03 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + def update_project_logo_props(apps, schema_editor): + Project = apps.get_model("db", "Project") + + bulk_update_project_logo = [] + # Iterate through projects and update logo_props + for project in Project.objects.all(): + project.logo_props["in_use"] = "emoji" if project.emoji else "icon" + project.logo_props["emoji"] = { + "value": project.emoji if project.emoji else "", + "url": "", + } + project.logo_props["icon"] = { + "name": ( + project.icon_prop.get("name", "") + if project.icon_prop + else "" + ), + "color": ( + project.icon_prop.get("color", "") + if project.icon_prop + else "" + ), + } + bulk_update_project_logo.append(project) + + # Bulk update logo_props for all projects + Project.objects.bulk_update( + bulk_update_project_logo, ["logo_props"], batch_size=1000 + ) + + dependencies = [ + ("db", "0060_cycle_progress_snapshot"), + ] + + operations = [ + migrations.AlterField( + model_name="issuelink", + name="url", + field=models.TextField(), + ), + migrations.AddField( + model_name="project", + name="logo_props", + field=models.JSONField(default=dict), + ), + migrations.RunPython(update_project_logo_props), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index b9317472433..bb4885d1404 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -107,6 +107,7 @@ class Project(BaseModel): close_in = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) + logo_props = models.JSONField(default=dict) default_state = models.ForeignKey( "db.State", on_delete=models.SET_NULL, diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index 86b35248289..a937341866c 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -1,12 +1,26 @@ import { EUserProjectRoles } from "constants/project"; import type { + IProjectViewProps, IUser, IUserLite, + IUserMemberLite, IWorkspace, IWorkspaceLite, TStateGroups, } from "."; +export type TProjectLogoProps = { + in_use: "emoji" | "icon"; + emoji?: { + value?: string; + url?: string; + }; + icon?: { + name?: string; + color?: string; + }; +}; + export interface IProject { archive_in: number; close_in: number; @@ -21,24 +35,13 @@ export interface IProject { default_assignee: IUser | string | null; default_state: string | null; description: string; - emoji: string | null; - emoji_and_icon: - | string - | { - name: string; - color: string; - } - | null; estimate: string | null; - icon_prop: { - name: string; - color: string; - } | null; id: string; identifier: string; is_deployed: boolean; is_favorite: boolean; is_member: boolean; + logo_props: TProjectLogoProps; member_role: EUserProjectRoles | null; members: IProjectMemberLite[]; name: string; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index c428dc7d284..5920f0b4983 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -132,11 +132,7 @@ export interface IUserProfileProjectSegregation { assigned_issues: number; completed_issues: number; created_issues: number; - emoji: string | null; - icon_prop: null; id: string; - identifier: string; - name: string; pending_issues: number; }[]; user_data: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 91a010a1eac..f80bcc6ae9e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,6 +23,7 @@ "@headlessui/react": "^1.7.17", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", + "emoji-picker-react": "^4.5.16", "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/emoji-icon-picker.tsx new file mode 100644 index 00000000000..42c367938b7 --- /dev/null +++ b/packages/ui/src/emoji/emoji-icon-picker.tsx @@ -0,0 +1,169 @@ +import React, { useState } from "react"; +import { usePopper } from "react-popper"; +import EmojiPicker, { EmojiClickData, Theme } from "emoji-picker-react"; +import { Popover, Tab } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; +// components +import { IconsList } from "./icons-list"; +// helpers +import { cn } from "../../helpers"; + +export enum EmojiIconPickerTypes { + EMOJI = "emoji", + ICON = "icon", +} + +type TChangeHandlerProps = + | { + type: EmojiIconPickerTypes.EMOJI; + value: EmojiClickData; + } + | { + type: EmojiIconPickerTypes.ICON; + value: { + name: string; + color: string; + }; + }; + +export type TCustomEmojiPicker = { + buttonClassName?: string; + className?: string; + closeOnSelect?: boolean; + defaultIconColor?: string; + defaultOpen?: EmojiIconPickerTypes; + disabled?: boolean; + dropdownClassName?: string; + label: React.ReactNode; + onChange: (value: TChangeHandlerProps) => void; + placement?: Placement; + searchPlaceholder?: string; + theme?: Theme; +}; + +const TABS_LIST = [ + { + key: EmojiIconPickerTypes.EMOJI, + title: "Emojis", + }, + { + key: EmojiIconPickerTypes.ICON, + title: "Icons", + }, +]; + +export const CustomEmojiIconPicker: React.FC = (props) => { + const { + buttonClassName, + className, + closeOnSelect = true, + defaultIconColor = "#5f5f5f", + defaultOpen = EmojiIconPickerTypes.EMOJI, + disabled = false, + dropdownClassName, + label, + onChange, + placement = "bottom-start", + searchPlaceholder = "Search", + theme, + } = props; + // refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 20, + }, + }, + ], + }); + + return ( + + {({ close }) => ( + <> + + + + +
+ tab.key === defaultOpen)} + > + + {TABS_LIST.map((tab) => ( + + cn("py-1 text-sm rounded border border-custom-border-200", { + "bg-custom-background-80": selected, + "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, + }) + } + > + {tab.title} + + ))} + + + + { + onChange({ + type: EmojiIconPickerTypes.EMOJI, + value: val, + }); + if (closeOnSelect) close(); + }} + height="20rem" + width="100%" + theme={theme} + searchPlaceholder={searchPlaceholder} + previewConfig={{ + showPreview: false, + }} + /> + + + { + onChange({ + type: EmojiIconPickerTypes.ICON, + value: val, + }); + if (closeOnSelect) close(); + }} + /> + + + +
+
+ + )} +
+ ); +}; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx new file mode 100644 index 00000000000..f55da881b47 --- /dev/null +++ b/packages/ui/src/emoji/icons-list.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from "react"; +// components +import { Input } from "../form-fields"; +// helpers +import { cn } from "../../helpers"; +// constants +import { MATERIAL_ICONS_LIST } from "./icons"; + +type TIconsListProps = { + defaultColor: string; + onChange: (val: { name: string; color: string }) => void; +}; + +const DEFAULT_COLORS = ["#ff6b00", "#8cc1ff", "#fcbe1d", "#18904f", "#adf672", "#05c3ff", "#5f5f5f"]; + +export const IconsList: React.FC = (props) => { + const { defaultColor, onChange } = props; + // states + const [activeColor, setActiveColor] = useState(defaultColor); + const [showHexInput, setShowHexInput] = useState(false); + const [hexValue, setHexValue] = useState(""); + + useEffect(() => { + if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); + else { + setHexValue(defaultColor.slice(1, 7)); + setShowHexInput(true); + } + }, [defaultColor]); + + return ( + <> +
+ {showHexInput ? ( +
+ + HEX + # + { + const value = e.target.value; + setHexValue(value); + if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`); + }} + className="flex-grow pl-0 text-xs text-custom-text-200" + mode="true-transparent" + autoFocus + /> +
+ ) : ( + DEFAULT_COLORS.map((curCol) => ( + + )) + )} + +
+
+ {MATERIAL_ICONS_LIST.map((icon) => ( + + ))} +
+ + ); +}; diff --git a/packages/ui/src/emoji/icons.ts b/packages/ui/src/emoji/icons.ts new file mode 100644 index 00000000000..72aacf18bb7 --- /dev/null +++ b/packages/ui/src/emoji/icons.ts @@ -0,0 +1,605 @@ +export const MATERIAL_ICONS_LIST = [ + { + name: "search", + }, + { + name: "home", + }, + { + name: "menu", + }, + { + name: "close", + }, + { + name: "settings", + }, + { + name: "done", + }, + { + name: "check_circle", + }, + { + name: "favorite", + }, + { + name: "add", + }, + { + name: "delete", + }, + { + name: "arrow_back", + }, + { + name: "star", + }, + { + name: "logout", + }, + { + name: "add_circle", + }, + { + name: "cancel", + }, + { + name: "arrow_drop_down", + }, + { + name: "more_vert", + }, + { + name: "check", + }, + { + name: "check_box", + }, + { + name: "toggle_on", + }, + { + name: "open_in_new", + }, + { + name: "refresh", + }, + { + name: "login", + }, + { + name: "radio_button_unchecked", + }, + { + name: "more_horiz", + }, + { + name: "apps", + }, + { + name: "radio_button_checked", + }, + { + name: "download", + }, + { + name: "remove", + }, + { + name: "toggle_off", + }, + { + name: "bolt", + }, + { + name: "arrow_upward", + }, + { + name: "filter_list", + }, + { + name: "delete_forever", + }, + { + name: "autorenew", + }, + { + name: "key", + }, + { + name: "sort", + }, + { + name: "sync", + }, + { + name: "add_box", + }, + { + name: "block", + }, + { + name: "restart_alt", + }, + { + name: "menu_open", + }, + { + name: "shopping_cart_checkout", + }, + { + name: "expand_circle_down", + }, + { + name: "backspace", + }, + { + name: "undo", + }, + { + name: "done_all", + }, + { + name: "do_not_disturb_on", + }, + { + name: "open_in_full", + }, + { + name: "double_arrow", + }, + { + name: "sync_alt", + }, + { + name: "zoom_in", + }, + { + name: "done_outline", + }, + { + name: "drag_indicator", + }, + { + name: "fullscreen", + }, + { + name: "star_half", + }, + { + name: "settings_accessibility", + }, + { + name: "reply", + }, + { + name: "exit_to_app", + }, + { + name: "unfold_more", + }, + { + name: "library_add", + }, + { + name: "cached", + }, + { + name: "select_check_box", + }, + { + name: "terminal", + }, + { + name: "change_circle", + }, + { + name: "disabled_by_default", + }, + { + name: "swap_horiz", + }, + { + name: "swap_vert", + }, + { + name: "app_registration", + }, + { + name: "download_for_offline", + }, + { + name: "close_fullscreen", + }, + { + name: "file_open", + }, + { + name: "minimize", + }, + { + name: "open_with", + }, + { + name: "dataset", + }, + { + name: "add_task", + }, + { + name: "start", + }, + { + name: "keyboard_voice", + }, + { + name: "create_new_folder", + }, + { + name: "forward", + }, + { + name: "download", + }, + { + name: "settings_applications", + }, + { + name: "compare_arrows", + }, + { + name: "redo", + }, + { + name: "zoom_out", + }, + { + name: "publish", + }, + { + name: "html", + }, + { + name: "token", + }, + { + name: "switch_access_shortcut", + }, + { + name: "fullscreen_exit", + }, + { + name: "sort_by_alpha", + }, + { + name: "delete_sweep", + }, + { + name: "indeterminate_check_box", + }, + { + name: "view_timeline", + }, + { + name: "settings_backup_restore", + }, + { + name: "arrow_drop_down_circle", + }, + { + name: "assistant_navigation", + }, + { + name: "sync_problem", + }, + { + name: "clear_all", + }, + { + name: "density_medium", + }, + { + name: "heart_plus", + }, + { + name: "filter_alt_off", + }, + { + name: "expand", + }, + { + name: "subdirectory_arrow_right", + }, + { + name: "download_done", + }, + { + name: "arrow_outward", + }, + { + name: "123", + }, + { + name: "swipe_left", + }, + { + name: "auto_mode", + }, + { + name: "saved_search", + }, + { + name: "place_item", + }, + { + name: "system_update_alt", + }, + { + name: "javascript", + }, + { + name: "search_off", + }, + { + name: "output", + }, + { + name: "select_all", + }, + { + name: "fit_screen", + }, + { + name: "swipe_up", + }, + { + name: "dynamic_form", + }, + { + name: "hide_source", + }, + { + name: "swipe_right", + }, + { + name: "switch_access_shortcut_add", + }, + { + name: "browse_gallery", + }, + { + name: "css", + }, + { + name: "density_small", + }, + { + name: "assistant_direction", + }, + { + name: "check_small", + }, + { + name: "youtube_searched_for", + }, + { + name: "move_up", + }, + { + name: "swap_horizontal_circle", + }, + { + name: "data_thresholding", + }, + { + name: "install_mobile", + }, + { + name: "move_down", + }, + { + name: "dataset_linked", + }, + { + name: "keyboard_command_key", + }, + { + name: "view_kanban", + }, + { + name: "swipe_down", + }, + { + name: "key_off", + }, + { + name: "transcribe", + }, + { + name: "send_time_extension", + }, + { + name: "swipe_down_alt", + }, + { + name: "swipe_left_alt", + }, + { + name: "swipe_right_alt", + }, + { + name: "swipe_up_alt", + }, + { + name: "keyboard_option_key", + }, + { + name: "cycle", + }, + { + name: "rebase", + }, + { + name: "rebase_edit", + }, + { + name: "empty_dashboard", + }, + { + name: "magic_exchange", + }, + { + name: "acute", + }, + { + name: "point_scan", + }, + { + name: "step_into", + }, + { + name: "cheer", + }, + { + name: "emoticon", + }, + { + name: "explosion", + }, + { + name: "water_bottle", + }, + { + name: "weather_hail", + }, + { + name: "syringe", + }, + { + name: "pill", + }, + { + name: "genetics", + }, + { + name: "allergy", + }, + { + name: "medical_mask", + }, + { + name: "body_fat", + }, + { + name: "barefoot", + }, + { + name: "infrared", + }, + { + name: "wrist", + }, + { + name: "metabolism", + }, + { + name: "conditions", + }, + { + name: "taunt", + }, + { + name: "altitude", + }, + { + name: "tibia", + }, + { + name: "footprint", + }, + { + name: "eyeglasses", + }, + { + name: "man_3", + }, + { + name: "woman_2", + }, + { + name: "rheumatology", + }, + { + name: "tornado", + }, + { + name: "landslide", + }, + { + name: "foggy", + }, + { + name: "severe_cold", + }, + { + name: "tsunami", + }, + { + name: "vape_free", + }, + { + name: "sign_language", + }, + { + name: "emoji_symbols", + }, + { + name: "clear_night", + }, + { + name: "emoji_food_beverage", + }, + { + name: "hive", + }, + { + name: "thunderstorm", + }, + { + name: "communication", + }, + { + name: "rocket", + }, + { + name: "pets", + }, + { + name: "public", + }, + { + name: "quiz", + }, + { + name: "mood", + }, + { + name: "gavel", + }, + { + name: "eco", + }, + { + name: "diamond", + }, + { + name: "forest", + }, + { + name: "rainy", + }, + { + name: "skull", + }, +]; diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts new file mode 100644 index 00000000000..97345413903 --- /dev/null +++ b/packages/ui/src/emoji/index.ts @@ -0,0 +1 @@ +export * from "./emoji-icon-picker"; diff --git a/packages/ui/src/form-fields/input.tsx b/packages/ui/src/form-fields/input.tsx index 6688d6778af..f73467621b6 100644 --- a/packages/ui/src/form-fields/input.tsx +++ b/packages/ui/src/form-fields/input.tsx @@ -1,4 +1,6 @@ import * as React from "react"; +// helpers +import { cn } from "../../helpers"; export interface InputProps extends React.InputHTMLAttributes { mode?: "primary" | "transparent" | "true-transparent"; @@ -16,17 +18,20 @@ const Input = React.forwardRef((props, ref) => { ref={ref} type={type} name={name} - className={`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ - mode === "primary" - ? "rounded-md border-[0.5px] border-custom-border-200" - : mode === "transparent" - ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" - : mode === "true-transparent" - ? "rounded border-none bg-transparent ring-0" - : "" - } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ - inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" - } ${className}`} + className={cn( + `block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ + mode === "primary" + ? "rounded-md border-[0.5px] border-custom-border-200" + : mode === "transparent" + ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" + : mode === "true-transparent" + ? "rounded border-none bg-transparent ring-0" + : "" + } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ + inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" + }`, + className + )} {...rest} /> ); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 218d375fa97..24b76c3e0e4 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -2,6 +2,7 @@ export * from "./avatar"; export * from "./breadcrumbs"; export * from "./badge"; export * from "./button"; +export * from "./emoji"; export * from "./dropdowns"; export * from "./form-fields"; export * from "./icons"; diff --git a/space/components/common/index.ts b/space/components/common/index.ts index f1c0b088e85..36cc3c898b5 100644 --- a/space/components/common/index.ts +++ b/space/components/common/index.ts @@ -1 +1,2 @@ export * from "./latest-feature-block"; +export * from "./project-logo"; diff --git a/space/components/common/project-logo.tsx b/space/components/common/project-logo.tsx new file mode 100644 index 00000000000..3d5887b2869 --- /dev/null +++ b/space/components/common/project-logo.tsx @@ -0,0 +1,34 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectLogoProps } from "@plane/types"; + +type Props = { + className?: string; + logo: TProjectLogoProps; +}; + +export const ProjectLogo: React.FC = (props) => { + const { className, logo } = props; + + if (logo.in_use === "icon" && logo.icon) + return ( + + {logo.icon.name} + + ); + + if (logo.in_use === "emoji" && logo.emoji) + return ( + + {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} + + ); + + return ; +}; diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index 0bc493b16f0..feb11ed13c0 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,15 +1,12 @@ import { useEffect } from "react"; - import Link from "next/link"; import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; // components -// import { NavbarSearch } from "./search"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; import { IssueFiltersDropdown } from "components/issues/filters"; +import { ProjectLogo } from "components/common"; // ui import { Avatar, Button } from "@plane/ui"; import { Briefcase } from "lucide-react"; @@ -19,18 +16,6 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; import { TIssueBoardKeys } from "types/issue"; -const renderEmoji = (emoji: string | { name: string; color: string }) => { - if (!emoji) return; - - if (typeof emoji === "object") - return ( - - {emoji.name} - - ); - else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); -}; - const IssueNavbar = observer(() => { const { project: projectStore, @@ -123,27 +108,15 @@ const IssueNavbar = observer(() => {
{/* project detail */}
-
- {projectStore.project ? ( - projectStore.project?.emoji ? ( - - {renderEmoji(projectStore.project.emoji)} - - ) : projectStore.project?.icon_prop ? ( -
- {renderEmoji(projectStore.project.icon_prop)} -
- ) : ( - - {projectStore.project?.name.charAt(0)} - - ) - ) : ( - - - - )} -
+ {projectStore.project ? ( + + + + ) : ( + + + + )}
{projectStore?.project?.name || `...`}
diff --git a/space/types/project.ts b/space/types/project.ts index e0e1bba9ef2..7e81d366c01 100644 --- a/space/types/project.ts +++ b/space/types/project.ts @@ -1,3 +1,5 @@ +import { TProjectLogoProps } from "@plane/types"; + export interface IWorkspace { id: string; name: string; @@ -9,10 +11,8 @@ export interface IProject { identifier: string; name: string; description: string; - icon: string; cover_image: string | null; - icon_prop: string | null; - emoji: string | null; + logo_props: TProjectLogoProps; } export interface IProjectSettings { diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 9a0eec22751..31812cb0089 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; type Props = { projectIds: string[]; @@ -28,15 +28,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro return (
- {project.emoji ? ( - {renderEmoji(project.emoji)} - ) : project.icon_prop ? ( -
{renderEmoji(project.icon_prop)}
- ) : ( - - {project?.name.charAt(0)} - - )} +
+ +

{truncateText(project.name, 20)}

({project.identifier}) diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index fb9ab90fa3d..26f97e8f9a8 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -3,8 +3,9 @@ import { useRouter } from "next/router"; // hooks import { NETWORK_CHOICES } from "constants/project"; import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useCycle, useMember, useModule, useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; // helpers // constants @@ -81,15 +82,9 @@ export const CustomAnalyticsSidebarHeader = observer(() => { ) : (
- {projectDetails?.emoji ? ( -
{renderEmoji(projectDetails.emoji)}
- ) : projectDetails?.icon_prop ? ( -
- {renderEmoji(projectDetails.icon_prop)} -
- ) : ( - - {projectDetails?.name.charAt(0)} + {projectDetails && ( + + )}

{projectDetails?.name}

diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index 72129df3fad..22e561ac82c 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -7,13 +7,13 @@ import { Avatar, AvatarGroup } from "@plane/ui"; import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; import { EUserWorkspaceRoles } from "constants/workspace"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components // ui // helpers // types import { TRecentProjectsWidgetResponse } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const WIDGET_KEY = "recent_projects"; @@ -38,17 +38,9 @@ const ProjectListItem: React.FC = observer((props) => {
- {projectDetails.emoji ? ( - - {renderEmoji(projectDetails.emoji)} - - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails.name.charAt(0)} - - )} +
+ +
diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 05b455e5e89..719b898029e 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -5,12 +5,12 @@ import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; +import { ProjectLogo } from "components/project"; // helpers // types import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; @@ -77,13 +77,11 @@ export const ProjectDropdown: React.FC = observer((props) => { query: `${projectDetails?.name}`, content: (
- - {projectDetails?.emoji - ? renderEmoji(projectDetails?.emoji) - : projectDetails?.icon_prop - ? renderEmoji(projectDetails?.icon_prop) - : null} - + {projectDetails && ( + + + + )} {projectDetails?.name}
), @@ -169,13 +167,9 @@ export const ProjectDropdown: React.FC = observer((props) => { showTooltip={showTooltip} variant={buttonVariant} > - {!hideIcon && ( - - {selectedProject?.emoji - ? renderEmoji(selectedProject?.emoji) - : selectedProject?.icon_prop - ? renderEmoji(selectedProject?.icon_prop) - : null} + {!hideIcon && selectedProject && ( + + )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/emoji-icon-picker/emojis.json b/web/components/emoji-icon-picker/emojis.json deleted file mode 100644 index 73b9b800fae..00000000000 --- a/web/components/emoji-icon-picker/emojis.json +++ /dev/null @@ -1,1090 +0,0 @@ -[ - "8986", - "8987", - "9193", - "9194", - "9195", - "9196", - "9197", - "9198", - "9199", - "9200", - "9201", - "9202", - "9203", - "9208", - "9209", - "9210", - "9410", - "9748", - "9749", - "9757", - "9800", - "9801", - "9802", - "9803", - "9804", - "9805", - "9806", - "9807", - "9808", - "9809", - "9810", - "9811", - "9823", - "9855", - "9875", - "9889", - "9898", - "9899", - "9917", - "9918", - "9924", - "9925", - "9934", - "9935", - "9937", - "9939", - "9940", - "9961", - "9962", - "9968", - "9969", - "9970", - "9971", - "9972", - "9973", - "9975", - "9976", - "9977", - "9978", - "9981", - "9986", - "9989", - "9992", - "9993", - "9994", - "9995", - "9996", - "9997", - "9999", - "10002", - "10004", - "10006", - "10013", - "10017", - "10024", - "10035", - "10036", - "10052", - "10055", - "10060", - "10062", - "10067", - "10068", - "10069", - "10071", - "10083", - "10084", - "10133", - "10134", - "10135", - "10145", - "10160", - "10175", - "10548", - "10549", - "11013", - "11014", - "11015", - "11035", - "11036", - "11088", - "11093", - "12336", - "12349", - "12951", - "12953", - "126980", - "127183", - "127344", - "127345", - "127358", - "127359", - "127374", - "127377", - "127378", - "127379", - "127380", - "127381", - "127382", - "127383", - "127384", - "127385", - "127386", - "127489", - "127490", - "127514", - "127535", - "127538", - "127539", - "127540", - "127541", - "127542", - "127543", - "127544", - "127545", - "127546", - "127568", - "127569", - "127744", - "127745", - "127746", - "127747", - "127748", - "127749", - "127750", - "127751", - "127752", - "127753", - "127754", - "127755", - "127756", - "127757", - "127758", - "127759", - "127760", - "127761", - "127762", - "127763", - "127764", - "127765", - "127766", - "127767", - "127768", - "127769", - "127770", - "127771", - "127772", - "127773", - "127774", - "127775", - "127776", - "127777", - "127780", - "127781", - "127782", - "127783", - "127784", - "127785", - "127786", - "127787", - "127788", - "127789", - "127790", - "127791", - "127792", - "127793", - "127794", - "127795", - "127796", - "127797", - "127798", - "127799", - "127800", - "127801", - "127802", - "127803", - "127804", - "127805", - "127806", - "127807", - "127808", - "127809", - "127810", - "127811", - "127812", - "127813", - "127814", - "127815", - "127816", - "127817", - "127818", - "127819", - "127820", - "127821", - "127822", - "127823", - "127824", - "127825", - "127826", - "127827", - "127828", - "127829", - "127830", - "127831", - "127832", - "127833", - "127834", - "127835", - "127836", - "127837", - "127838", - "127839", - "127840", - "127841", - "127842", - "127843", - "127844", - "127845", - "127846", - "127847", - "127848", - "127849", - "127850", - "127851", - "127852", - "127853", - "127854", - "127855", - "127856", - "127857", - "127858", - "127859", - "127860", - "127861", - "127862", - "127863", - "127864", - "127865", - "127866", - "127867", - "127868", - "127869", - "127870", - "127871", - "127872", - "127873", - "127874", - "127875", - "127876", - "127877", - "127878", - "127879", - "127880", - "127881", - "127882", - "127883", - "127884", - "127885", - "127886", - "127887", - "127888", - "127889", - "127890", - "127891", - "127894", - "127895", - "127897", - "127898", - "127899", - "127902", - "127903", - "127904", - "127905", - "127906", - "127907", - "127908", - "127909", - "127910", - "127911", - "127912", - "127913", - "127914", - "127915", - "127916", - "127917", - "127918", - "127919", - "127920", - "127921", - "127922", - "127923", - "127924", - "127925", - "127926", - "127927", - "127928", - "127929", - "127930", - "127931", - "127932", - "127933", - "127934", - "127935", - "127936", - "127937", - "127938", - "127939", - "127940", - "127941", - "127942", - "127943", - "127944", - "127945", - "127946", - "127947", - "127948", - "127949", - "127950", - "127951", - "127952", - "127953", - "127954", - "127955", - "127956", - "127957", - "127958", - "127959", - "127960", - "127961", - "127962", - "127963", - "127964", - "127965", - "127966", - "127967", - "127968", - "127969", - "127970", - "127971", - "127972", - "127973", - "127974", - "127975", - "127976", - "127977", - "127978", - "127979", - "127980", - "127981", - "127982", - "127983", - "127984", - "127987", - "127988", - "127989", - "127991", - "127992", - "127993", - "127994", - "127995", - "127996", - "127997", - "127998", - "127999", - "128000", - "128001", - "128002", - "128003", - "128004", - "128005", - "128006", - "128007", - "128008", - "128009", - "128010", - "128011", - "128012", - "128013", - "128014", - "128015", - "128016", - "128017", - "128018", - "128019", - "128020", - "128021", - "128022", - "128023", - "128024", - "128025", - "128026", - "128027", - "128028", - "128029", - "128030", - "128031", - "128032", - "128033", - "128034", - "128035", - "128036", - "128037", - "128038", - "128039", - "128040", - "128041", - "128042", - "128043", - "128044", - "128045", - "128046", - "128047", - "128048", - "128049", - "128050", - "128051", - "128052", - "128053", - "128054", - "128055", - "128056", - "128057", - "128058", - "128059", - "128060", - "128061", - "128062", - "128063", - "128064", - "128065", - "128066", - "128067", - "128068", - "128069", - "128070", - "128071", - "128072", - "128073", - "128074", - "128075", - "128076", - "128077", - "128078", - "128079", - "128080", - "128081", - "128082", - "128083", - "128084", - "128085", - "128086", - "128087", - "128088", - "128089", - "128090", - "128091", - "128092", - "128093", - "128094", - "128095", - "128096", - "128097", - "128098", - "128099", - "128100", - "128101", - "128102", - "128103", - "128104", - "128105", - "128106", - "128107", - "128108", - "128109", - "128110", - "128111", - "128112", - "128113", - "128114", - "128115", - "128116", - "128117", - "128118", - "128119", - "128120", - "128121", - "128122", - "128123", - "128124", - "128125", - "128126", - "128127", - "128128", - "128129", - "128130", - "128131", - "128132", - "128133", - "128134", - "128135", - "128136", - "128137", - "128138", - "128139", - "128140", - "128141", - "128142", - "128143", - "128144", - "128145", - "128146", - "128147", - "128148", - "128149", - "128150", - "128151", - "128152", - "128153", - "128154", - "128155", - "128156", - "128157", - "128158", - "128159", - "128160", - "128161", - "128162", - "128163", - "128164", - "128165", - "128166", - "128167", - "128168", - "128169", - "128170", - "128171", - "128172", - "128173", - "128174", - "128175", - "128176", - "128177", - "128178", - "128179", - "128180", - "128181", - "128182", - "128183", - "128184", - "128185", - "128186", - "128187", - "128188", - "128189", - "128190", - "128191", - "128192", - "128193", - "128194", - "128195", - "128196", - "128197", - "128198", - "128199", - "128200", - "128201", - "128202", - "128203", - "128204", - "128205", - "128206", - "128207", - "128208", - "128209", - "128210", - "128211", - "128212", - "128213", - "128214", - "128215", - "128216", - "128217", - "128218", - "128219", - "128220", - "128221", - "128222", - "128223", - "128224", - "128225", - "128226", - "128227", - "128228", - "128229", - "128230", - "128231", - "128232", - "128233", - "128234", - "128235", - "128236", - "128237", - "128238", - "128239", - "128240", - "128241", - "128242", - "128243", - "128244", - "128245", - "128246", - "128247", - "128248", - "128249", - "128250", - "128251", - "128252", - "128253", - "128255", - "128256", - "128257", - "128258", - "128259", - "128260", - "128261", - "128262", - "128263", - "128264", - "128265", - "128266", - "128267", - "128268", - "128269", - "128270", - "128271", - "128272", - "128273", - "128274", - "128275", - "128276", - "128277", - "128278", - "128279", - "128280", - "128281", - "128282", - "128283", - "128284", - "128285", - "128286", - "128287", - "128288", - "128289", - "128290", - "128291", - "128292", - "128293", - "128294", - "128295", - "128296", - "128297", - "128298", - "128299", - "128300", - "128301", - "128302", - "128303", - "128304", - "128305", - "128306", - "128307", - "128308", - "128309", - "128310", - "128311", - "128312", - "128313", - "128314", - "128315", - "128316", - "128317", - "128329", - "128330", - "128331", - "128332", - "128333", - "128334", - "128336", - "128337", - "128338", - "128339", - "128340", - "128341", - "128342", - "128343", - "128344", - "128345", - "128346", - "128347", - "128348", - "128349", - "128350", - "128351", - "128352", - "128353", - "128354", - "128355", - "128356", - "128357", - "128358", - "128359", - "128367", - "128368", - "128371", - "128372", - "128373", - "128374", - "128375", - "128376", - "128377", - "128378", - "128391", - "128394", - "128395", - "128396", - "128397", - "128400", - "128405", - "128406", - "128420", - "128421", - "128424", - "128433", - "128434", - "128444", - "128450", - "128451", - "128452", - "128465", - "128466", - "128467", - "128476", - "128477", - "128478", - "128481", - "128483", - "128488", - "128495", - "128499", - "128506", - "128507", - "128508", - "128509", - "128510", - "128511", - "128512", - "128513", - "128514", - "128515", - "128516", - "128517", - "128518", - "128519", - "128520", - "128521", - "128522", - "128523", - "128524", - "128525", - "128526", - "128527", - "128528", - "128529", - "128530", - "128531", - "128532", - "128533", - "128534", - "128535", - "128536", - "128537", - "128538", - "128539", - "128540", - "128541", - "128542", - "128543", - "128544", - "128545", - "128546", - "128547", - "128548", - "128549", - "128550", - "128551", - "128552", - "128553", - "128554", - "128555", - "128556", - "128557", - "128558", - "128559", - "128560", - "128561", - "128562", - "128563", - "128564", - "128565", - "128566", - "128567", - "128568", - "128569", - "128570", - "128571", - "128572", - "128573", - "128574", - "128575", - "128576", - "128577", - "128578", - "128579", - "128580", - "128581", - "128582", - "128583", - "128584", - "128585", - "128586", - "128587", - "128588", - "128589", - "128590", - "128591", - "128640", - "128641", - "128642", - "128643", - "128644", - "128645", - "128646", - "128647", - "128648", - "128649", - "128650", - "128651", - "128652", - "128653", - "128654", - "128655", - "128656", - "128657", - "128658", - "128659", - "128660", - "128661", - "128662", - "128663", - "128664", - "128665", - "128666", - "128667", - "128668", - "128669", - "128670", - "128671", - "128672", - "128673", - "128674", - "128675", - "128676", - "128677", - "128678", - "128679", - "128680", - "128681", - "128682", - "128683", - "128684", - "128685", - "128686", - "128687", - "128688", - "128689", - "128690", - "128691", - "128692", - "128693", - "128694", - "128695", - "128696", - "128697", - "128698", - "128699", - "128700", - "128701", - "128702", - "128703", - "128704", - "128705", - "128706", - "128707", - "128708", - "128709", - "128715", - "128716", - "128717", - "128718", - "128719", - "128720", - "128721", - "128722", - "128736", - "128737", - "128738", - "128739", - "128740", - "128741", - "128745", - "128747", - "128748", - "128752", - "128755", - "128756", - "128757", - "128758", - "128759", - "128760", - "128761", - "128762", - "129296", - "129297", - "129298", - "129299", - "129300", - "129301", - "129302", - "129303", - "129304", - "129305", - "129306", - "129307", - "129308", - "129309", - "129310", - "129311", - "129312", - "129313", - "129314", - "129315", - "129316", - "129317", - "129318", - "129319", - "129320", - "129321", - "129322", - "129323", - "129324", - "129325", - "129326", - "129327", - "129328", - "129329", - "129330", - "129331", - "129332", - "129333", - "129334", - "129335", - "129336", - "129337", - "129338", - "129340", - "129341", - "129342", - "129344", - "129345", - "129346", - "129347", - "129348", - "129349", - "129351", - "129352", - "129353", - "129354", - "129355", - "129356", - "129357", - "129358", - "129359", - "129360", - "129361", - "129362", - "129363", - "129364", - "129365", - "129366", - "129367", - "129368", - "129369", - "129370", - "129371", - "129372", - "129373", - "129374", - "129375", - "129376", - "129377", - "129378", - "129379", - "129380", - "129381", - "129382", - "129383", - "129384", - "129385", - "129386", - "129387", - "129408", - "129409", - "129410", - "129411", - "129412", - "129413", - "129414", - "129415", - "129416", - "129417", - "129418", - "129419", - "129420", - "129421", - "129422", - "129423", - "129424", - "129425", - "129426", - "129427", - "129428", - "129429", - "129430", - "129431", - "129472", - "129488", - "129489", - "129490", - "129491", - "129492", - "129493", - "129494", - "129495", - "129496", - "129497", - "129498", - "129499", - "129500", - "129501", - "129502", - "129503", - "129504", - "129505", - "129506", - "129507", - "129508", - "129509", - "129510" -] diff --git a/web/components/emoji-icon-picker/helpers.ts b/web/components/emoji-icon-picker/helpers.ts deleted file mode 100644 index ab59a7b0763..00000000000 --- a/web/components/emoji-icon-picker/helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const saveRecentEmoji = (emoji: string) => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - if (recentEmojisArray.includes(emoji)) { - const index = recentEmojisArray.indexOf(emoji); - recentEmojisArray.splice(index, 1); - } - recentEmojisArray.unshift(emoji); - if (recentEmojisArray.length > 18) { - recentEmojisArray.pop(); - } - localStorage.setItem("recentEmojis", recentEmojisArray.join(",")); - } else { - localStorage.setItem("recentEmojis", emoji); - } -}; - -export const getRecentEmojis = () => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - return recentEmojisArray; - } - return []; -}; diff --git a/web/components/emoji-icon-picker/icons.json b/web/components/emoji-icon-picker/icons.json deleted file mode 100644 index f844f22d451..00000000000 --- a/web/components/emoji-icon-picker/icons.json +++ /dev/null @@ -1,607 +0,0 @@ -{ - "material_rounded": [ - { - "name": "search" - }, - { - "name": "home" - }, - { - "name": "menu" - }, - { - "name": "close" - }, - { - "name": "settings" - }, - { - "name": "done" - }, - { - "name": "check_circle" - }, - { - "name": "favorite" - }, - { - "name": "add" - }, - { - "name": "delete" - }, - { - "name": "arrow_back" - }, - { - "name": "star" - }, - { - "name": "logout" - }, - { - "name": "add_circle" - }, - { - "name": "cancel" - }, - { - "name": "arrow_drop_down" - }, - { - "name": "more_vert" - }, - { - "name": "check" - }, - { - "name": "check_box" - }, - { - "name": "toggle_on" - }, - { - "name": "open_in_new" - }, - { - "name": "refresh" - }, - { - "name": "login" - }, - { - "name": "radio_button_unchecked" - }, - { - "name": "more_horiz" - }, - { - "name": "apps" - }, - { - "name": "radio_button_checked" - }, - { - "name": "download" - }, - { - "name": "remove" - }, - { - "name": "toggle_off" - }, - { - "name": "bolt" - }, - { - "name": "arrow_upward" - }, - { - "name": "filter_list" - }, - { - "name": "delete_forever" - }, - { - "name": "autorenew" - }, - { - "name": "key" - }, - { - "name": "sort" - }, - { - "name": "sync" - }, - { - "name": "add_box" - }, - { - "name": "block" - }, - { - "name": "restart_alt" - }, - { - "name": "menu_open" - }, - { - "name": "shopping_cart_checkout" - }, - { - "name": "expand_circle_down" - }, - { - "name": "backspace" - }, - { - "name": "undo" - }, - { - "name": "done_all" - }, - { - "name": "do_not_disturb_on" - }, - { - "name": "open_in_full" - }, - { - "name": "double_arrow" - }, - { - "name": "sync_alt" - }, - { - "name": "zoom_in" - }, - { - "name": "done_outline" - }, - { - "name": "drag_indicator" - }, - { - "name": "fullscreen" - }, - { - "name": "star_half" - }, - { - "name": "settings_accessibility" - }, - { - "name": "reply" - }, - { - "name": "exit_to_app" - }, - { - "name": "unfold_more" - }, - { - "name": "library_add" - }, - { - "name": "cached" - }, - { - "name": "select_check_box" - }, - { - "name": "terminal" - }, - { - "name": "change_circle" - }, - { - "name": "disabled_by_default" - }, - { - "name": "swap_horiz" - }, - { - "name": "swap_vert" - }, - { - "name": "app_registration" - }, - { - "name": "download_for_offline" - }, - { - "name": "close_fullscreen" - }, - { - "name": "file_open" - }, - { - "name": "minimize" - }, - { - "name": "open_with" - }, - { - "name": "dataset" - }, - { - "name": "add_task" - }, - { - "name": "start" - }, - { - "name": "keyboard_voice" - }, - { - "name": "create_new_folder" - }, - { - "name": "forward" - }, - { - "name": "download" - }, - { - "name": "settings_applications" - }, - { - "name": "compare_arrows" - }, - { - "name": "redo" - }, - { - "name": "zoom_out" - }, - { - "name": "publish" - }, - { - "name": "html" - }, - { - "name": "token" - }, - { - "name": "switch_access_shortcut" - }, - { - "name": "fullscreen_exit" - }, - { - "name": "sort_by_alpha" - }, - { - "name": "delete_sweep" - }, - { - "name": "indeterminate_check_box" - }, - { - "name": "view_timeline" - }, - { - "name": "settings_backup_restore" - }, - { - "name": "arrow_drop_down_circle" - }, - { - "name": "assistant_navigation" - }, - { - "name": "sync_problem" - }, - { - "name": "clear_all" - }, - { - "name": "density_medium" - }, - { - "name": "heart_plus" - }, - { - "name": "filter_alt_off" - }, - { - "name": "expand" - }, - { - "name": "subdirectory_arrow_right" - }, - { - "name": "download_done" - }, - { - "name": "arrow_outward" - }, - { - "name": "123" - }, - { - "name": "swipe_left" - }, - { - "name": "auto_mode" - }, - { - "name": "saved_search" - }, - { - "name": "place_item" - }, - { - "name": "system_update_alt" - }, - { - "name": "javascript" - }, - { - "name": "search_off" - }, - { - "name": "output" - }, - { - "name": "select_all" - }, - { - "name": "fit_screen" - }, - { - "name": "swipe_up" - }, - { - "name": "dynamic_form" - }, - { - "name": "hide_source" - }, - { - "name": "swipe_right" - }, - { - "name": "switch_access_shortcut_add" - }, - { - "name": "browse_gallery" - }, - { - "name": "css" - }, - { - "name": "density_small" - }, - { - "name": "assistant_direction" - }, - { - "name": "check_small" - }, - { - "name": "youtube_searched_for" - }, - { - "name": "move_up" - }, - { - "name": "swap_horizontal_circle" - }, - { - "name": "data_thresholding" - }, - { - "name": "install_mobile" - }, - { - "name": "move_down" - }, - { - "name": "dataset_linked" - }, - { - "name": "keyboard_command_key" - }, - { - "name": "view_kanban" - }, - { - "name": "swipe_down" - }, - { - "name": "key_off" - }, - { - "name": "transcribe" - }, - { - "name": "send_time_extension" - }, - { - "name": "swipe_down_alt" - }, - { - "name": "swipe_left_alt" - }, - { - "name": "swipe_right_alt" - }, - { - "name": "swipe_up_alt" - }, - { - "name": "keyboard_option_key" - }, - { - "name": "cycle" - }, - { - "name": "rebase" - }, - { - "name": "rebase_edit" - }, - { - "name": "empty_dashboard" - }, - { - "name": "magic_exchange" - }, - { - "name": "acute" - }, - { - "name": "point_scan" - }, - { - "name": "step_into" - }, - { - "name": "cheer" - }, - { - "name": "emoticon" - }, - { - "name": "explosion" - }, - { - "name": "water_bottle" - }, - { - "name": "weather_hail" - }, - { - "name": "syringe" - }, - { - "name": "pill" - }, - { - "name": "genetics" - }, - { - "name": "allergy" - }, - { - "name": "medical_mask" - }, - { - "name": "body_fat" - }, - { - "name": "barefoot" - }, - { - "name": "infrared" - }, - { - "name": "wrist" - }, - { - "name": "metabolism" - }, - { - "name": "conditions" - }, - { - "name": "taunt" - }, - { - "name": "altitude" - }, - { - "name": "tibia" - }, - { - "name": "footprint" - }, - { - "name": "eyeglasses" - }, - { - "name": "man_3" - }, - { - "name": "woman_2" - }, - { - "name": "rheumatology" - }, - { - "name": "tornado" - }, - { - "name": "landslide" - }, - { - "name": "foggy" - }, - { - "name": "severe_cold" - }, - { - "name": "tsunami" - }, - { - "name": "vape_free" - }, - { - "name": "sign_language" - }, - { - "name": "emoji_symbols" - }, - { - "name": "clear_night" - }, - { - "name": "emoji_food_beverage" - }, - { - "name": "hive" - }, - { - "name": "thunderstorm" - }, - { - "name": "communication" - }, - { - "name": "rocket" - }, - { - "name": "pets" - }, - { - "name": "public" - }, - { - "name": "quiz" - }, - { - "name": "mood" - }, - { - "name": "gavel" - }, - { - "name": "eco" - }, - { - "name": "diamond" - }, - { - "name": "forest" - }, - { - "name": "rainy" - }, - { - "name": "skull" - } - ] -} diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx deleted file mode 100644 index b9211a718e2..00000000000 --- a/web/components/emoji-icon-picker/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useEffect, useState, useRef } from "react"; -// headless ui -import { TwitterPicker } from "react-color"; -import { Tab, Transition, Popover } from "@headlessui/react"; -// react colors -// hooks -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// types -// emojis -import emojis from "./emojis.json"; -import { getRecentEmojis, saveRecentEmoji } from "./helpers"; -import icons from "./icons.json"; -// helpers -import { Props } from "./types"; - -const tabOptions = [ - { - key: "emoji", - title: "Emoji", - }, - { - key: "icon", - title: "Icon", - }, -]; - -const EmojiIconPicker: React.FC = (props) => { - const { label, value, onChange, onIconColorChange, disabled = false } = props; - // states - const [isOpen, setIsOpen] = useState(false); - const [openColorPicker, setOpenColorPicker] = useState(false); - const [activeColor, setActiveColor] = useState("rgb(var(--color-text-200))"); - const [recentEmojis, setRecentEmojis] = useState([]); - - const buttonRef = useRef(null); - const emojiPickerRef = useRef(null); - - useEffect(() => { - setRecentEmojis(getRecentEmojis()); - }, []); - - useEffect(() => { - if (!value || value?.length === 0) onChange(getRandomEmoji()); - }, [value, onChange]); - - useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false)); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), buttonRef, emojiPickerRef); - - return ( - - setIsOpen((prev) => !prev)} - className="outline-none flex items-center justify-center" - disabled={disabled} - > - {label} - - - -
- - - {tabOptions.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - - - - {recentEmojis.length > 0 && ( -
-

Recent

-
- {recentEmojis.map((emoji) => ( - - ))} -
-
- )} -
-
-
- {emojis.map((emoji) => ( - - ))} -
-
-
-
- -
-
- {["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => ( - setActiveColor(curCol)} - /> - ))} - -
-
- { - setActiveColor(color.hex); - if (onIconColorChange) onIconColorChange(color.hex); - }} - triangle="hide" - width="205px" - /> -
-
-
-
- {icons.material_rounded.map((icon, index) => ( - - ))} -
-
-
-
-
-
-
-
-
- ); -}; - -export default EmojiIconPicker; diff --git a/web/components/emoji-icon-picker/types.d.ts b/web/components/emoji-icon-picker/types.d.ts deleted file mode 100644 index 8a0b54342dd..00000000000 --- a/web/components/emoji-icon-picker/types.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Props = { - label: React.ReactNode; - value: any; - onChange: ( - data: - | string - | { - name: string; - color: string; - } - ) => void; - onIconColorChange?: (data: any) => void; - disabled?: boolean; - tabIndex?: number; -}; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 18d0543c01d..46890011041 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -13,7 +13,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage"; // helpers // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { @@ -163,13 +163,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -209,7 +205,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => )} + {currentProjectCycleIds?.map((cycleId) => ( + + ))} } /> diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index a0ab19ec72c..22637147f1c 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -11,14 +11,15 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; import { TCycleLayout } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const CyclesHeader: FC = observer(() => { // router const router = useRouter(); + const { workspaceSlug } = router.query; // store hooks const { commandPalette: { toggleCreateCycleModal }, @@ -32,9 +33,6 @@ export const CyclesHeader: FC = observer(() => { const canUserCreateCycle = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const { workspaceSlug } = router.query as { - workspaceSlug: string; - }; const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( @@ -58,13 +56,9 @@ export const CyclesHeader: FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index ca3a84e3b75..b42b8774ad7 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -13,7 +13,6 @@ import { ModuleMobileHeader } from "components/modules/module-mobile-header"; import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage"; // helpers // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { @@ -64,11 +64,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -100,7 +96,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -119,7 +121,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] ); @@ -127,7 +135,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -135,7 +149,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + moduleId?.toString() + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -166,13 +186,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -212,7 +228,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => )} + {projectModuleIds?.map((moduleId) => ( + + ))} } /> diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index b942b7b136f..a1233ae5211 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -10,11 +10,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -// helper -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { ProjectLogo } from "components/project"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -46,13 +45,9 @@ export const ModulesListHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 0eed7217875..2c05d95fadb 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -8,9 +8,9 @@ import { Breadcrumbs, Button } from "@plane/ui"; // helpers import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { renderEmoji } from "helpers/emoji.helper"; // components import { useApplication, usePage, useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export interface IPagesHeaderProps { showButton?: boolean; @@ -42,13 +42,9 @@ export const PageDetailsHeader: FC = observer((props) => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index b5ce74fc5b8..e45d1a9fe09 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -8,10 +8,10 @@ import { Breadcrumbs, Button } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // constants // components import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const PagesHeader = observer(() => { // router @@ -43,13 +43,9 @@ export const PagesHeader = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 8752e739630..86dae643d30 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -7,8 +7,9 @@ import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { ISSUE_DETAILS } from "constants/fetch-keys"; -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; // ui // types import { IssueArchiveService } from "services/issue"; @@ -52,13 +53,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index 8ade61aae36..db208aa21cc 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -12,10 +12,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectArchivedIssuesHeader: FC = observer(() => { // router @@ -91,13 +91,9 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 3fd0cb39939..4f292962116 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -10,9 +10,9 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect // ui // helper import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { renderEmoji } from "helpers/emoji.helper"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectDraftIssueHeader: FC = observer(() => { // router @@ -86,13 +86,9 @@ export const ProjectDraftIssueHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b89fbaaacb1..0e1bdcd1e13 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -10,8 +10,8 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CreateInboxIssueModal } from "components/inbox"; // helper -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -35,13 +35,9 @@ export const ProjectInboxHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 2f6349e613c..b9343a15cac 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -9,12 +9,12 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { ISSUE_DETAILS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useProject } from "hooks/store"; // ui // helpers // services import { IssueService } from "services/issue"; +import { ProjectLogo } from "components/project"; // constants // components @@ -51,13 +51,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 8e8807fdbf6..19eaf4f4fb4 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -11,7 +11,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { useApplication, useEventTracker, @@ -21,11 +20,12 @@ import { useUser, useMember, } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // components // ui // types -import { useIssues } from "hooks/store/use-issues"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants // helper @@ -123,17 +123,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} icon={ currentProjectDetails ? ( - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) ) : ( diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index 87b2e507e0d..817d842b48d 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -7,9 +7,9 @@ import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; // constants // components @@ -44,13 +44,9 @@ export const ProjectSettingHeader: FC = observer((props) href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index eea21143164..4abc3edf944 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -15,7 +15,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; import { useApplication, @@ -29,6 +28,7 @@ import { useUser, } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router @@ -119,17 +119,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 3b4d7fb203e..99533189a18 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -8,9 +8,9 @@ import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // helpers import { EUserProjectRoles } from "constants/project"; -import { renderEmoji } from "helpers/emoji.helper"; // constants import { useApplication, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectViewsHeader: React.FC = observer(() => { // router @@ -42,17 +42,9 @@ export const ProjectViewsHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 24e8fd33848..84e81b6e806 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks -import { renderEmoji } from "helpers/emoji.helper"; import { useProject } from "hooks/store"; -// helpers +// components +import { ProjectLogo } from "components/project"; type Props = { handleRemove: (val: string) => void; @@ -25,15 +25,9 @@ export const AppliedProjectFilters: React.FC = observer((props) => { return (
- {projectDetails.emoji ? ( - {renderEmoji(projectDetails.emoji)} - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails?.name.charAt(0)} - - )} + + + {projectDetails.name} {editable && (
{userProjectsData.project_data.map((project, index) => { + const projectDetails = getProjectById(project.id); + const totalIssues = project.created_issues + project.assigned_issues + project.pending_issues + project.completed_issues; @@ -138,26 +142,18 @@ export const ProfileSidebar = observer(() => { ? 0 : Math.round((project.completed_issues / project.assigned_issues) * 100); + if (!projectDetails) return null; + return ( {({ open }) => (
- {project.emoji ? ( -
- {renderEmoji(project.emoji)} -
- ) : project.icon_prop ? ( -
- {renderEmoji(project.icon_prop)} -
- ) : ( -
- {project?.name.charAt(0)} -
- )} -
{project.name}
+ + + +
{projectDetails.name}
{project.assigned_issues > 0 && ( diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index c74e9ee75e9..08aec43fa51 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -6,14 +6,14 @@ import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // components -import { DeleteProjectModal, JoinProjectModal, EUserProjectRoles } from "components/project"; +import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "components/project"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // hooks import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; // constants export type ProjectCardProps = { @@ -123,13 +123,9 @@ export const ProjectCard: React.FC = observer((props) => {
-
- - {project.emoji - ? renderEmoji(project.emoji) - : project.icon_prop - ? renderEmoji(project.icon_prop) - : null} +
+ +
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 01cbb5888a0..7a66c3a305f 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -4,19 +4,30 @@ import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { X } from "lucide-react"; // ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, +} from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; import { MemberDropdown } from "components/dropdowns"; -import EmojiIconPicker from "components/emoji-icon-picker"; // constants import { PROJECT_CREATED } from "constants/event-tracker"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; import { EUserWorkspaceRoles } from "constants/workspace"; // helpers -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; // hooks import { useEventTracker, useProject, useUser } from "hooks/store"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +import { ProjectLogo } from "./project-logo"; +import { IProject } from "@plane/types"; type Props = { isOpen: boolean; @@ -29,6 +40,21 @@ interface IIsGuestCondition { onClose: () => void; } +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + const IsGuestCondition: FC = ({ onClose }) => { useEffect(() => { onClose(); @@ -42,19 +68,6 @@ const IsGuestCondition: FC = ({ onClose }) => { return null; }; -export interface ICreateProjectForm { - name: string; - identifier: string; - description: string; - emoji_and_icon: string; - network: number; - project_lead_member: string; - project_lead: string; - cover_image: string; - icon_prop: any; - emoji: string; -} - export const CreateProjectModal: FC = observer((props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // store @@ -66,7 +79,6 @@ export const CreateProjectModal: FC = observer((props) => { // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // form info - const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { formState: { errors, isSubmitting }, handleSubmit, @@ -74,28 +86,20 @@ export const CreateProjectModal: FC = observer((props) => { control, watch, setValue, - } = useForm({ - defaultValues: { - cover_image, - description: "", - emoji_and_icon: getRandomEmoji(), - identifier: "", - name: "", - network: 2, - project_lead: undefined, - }, + } = useForm({ + defaultValues, reValidateMode: "onChange", }); - const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - if (currentWorkspaceRole && isOpen) if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; const handleClose = () => { onClose(); setIsChangeInIdentifierRequired(true); - reset(); + setTimeout(() => { + reset(); + }, 300); }; const handleAddToFavorites = (projectId: string) => { @@ -110,18 +114,11 @@ export const CreateProjectModal: FC = observer((props) => { }); }; - const onSubmit = async (formData: ICreateProjectForm) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { emoji_and_icon, project_lead_member, ...payload } = formData; - - if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon; - else payload.emoji = formData.emoji_and_icon; - - payload.project_lead = formData.project_lead_member; + const onSubmit = async (formData: Partial) => { // Upper case identifier - payload.identifier = payload.identifier.toUpperCase(); + formData.identifier = formData.identifier?.toUpperCase(); - return createProject(workspaceSlug.toString(), payload) + return createProject(workspaceSlug.toString(), formData) .then((res) => { const newPayload = { ...res, @@ -151,7 +148,7 @@ export const CreateProjectModal: FC = observer((props) => { captureProjectEvent({ eventName: PROJECT_CREATED, payload: { - ...payload, + ...formData, state: "FAILED", }, }); @@ -165,13 +162,13 @@ export const CreateProjectModal: FC = observer((props) => { return; } if (e.target.value === "") setValue("identifier", ""); - else setValue("identifier", e.target.value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "").substring(0, 5)); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); onChange(e); }; const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { const { value } = e.target; - const alphanumericValue = value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); + const alphanumericValue = projectIdentifierSanitizer(value); setIsChangeInIdentifierRequired(false); onChange(alphanumericValue); }; @@ -204,11 +201,11 @@ export const CreateProjectModal: FC = observer((props) => { >
- {watch("cover_image") !== null && ( + {watch("cover_image") && ( Cover Image )} @@ -218,30 +215,50 @@ export const CreateProjectModal: FC = observer((props) => {
- { - setValue("cover_image", image); - }} + ( + + )} />
( - - {value ? renderEmoji(value) : "Icon"} -
+ + + + } + onChange={(val) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={ + value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON } - onChange={onChange} - value={value} - tabIndex={10} /> )} /> @@ -275,7 +292,9 @@ export const CreateProjectModal: FC = observer((props) => { /> )} /> - {errors?.name?.message} + + <>{errors?.name?.message} +
= observer((props) => { /> )} /> - {errors?.identifier?.message} + + <>{errors?.identifier?.message} +
= observer((props) => { ( -
- - {currentNetwork ? ( - <> - - {currentNetwork.label} - - ) : ( - Select Network - )} -
- } - placement="bottom-start" - noChevron - tabIndex={4} - > - {NETWORK_CHOICES.map((network) => ( - -
- -
-

{network.label}

-

{network.description}

-
+ render={({ field: { onChange, value } }) => { + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value); + + return ( +
+ + {currentNetwork ? ( + <> + + {currentNetwork.label} + + ) : ( + Select network + )}
- - ))} - -
- )} + } + placement="bottom-start" + noChevron + tabIndex={4} + > + {NETWORK_CHOICES.map((network) => ( + +
+ +
+

{network.label}

+

{network.description}

+
+
+
+ ))} + +
+ ); + }} /> ( -
- -
- )} + render={({ field: { value, onChange } }) => { + if (value === undefined || value === null || typeof value === "string") + return ( +
+ +
+ ); + else return <>; + }} />
@@ -396,7 +425,7 @@ export const CreateProjectModal: FC = observer((props) => { Cancel
diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 25186e08e6c..1ef7ee226a8 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -3,22 +3,31 @@ import { Controller, useForm } from "react-hook-form"; // icons import { Lock } from "lucide-react"; // ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; +import { + Button, + CustomSelect, + Input, + TextArea, + TOAST_TYPE, + setToast, + CustomEmojiIconPicker, + EmojiIconPickerTypes, +} from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; -import EmojiIconPicker from "components/emoji-icon-picker"; // constants import { PROJECT_UPDATED } from "constants/event-tracker"; import { NETWORK_CHOICES } from "constants/project"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // hooks import { useEventTracker, useProject } from "hooks/store"; // services import { ProjectService } from "services/project"; // types import { IProject, IWorkspace } from "@plane/types"; +import { ProjectLogo } from "./project-logo"; +import { convertHexEmojiToDecimal } from "helpers/emoji.helper"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; @@ -46,7 +55,6 @@ export const ProjectDetailsForm: FC = (props) => { } = useForm({ defaultValues: { ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }, }); @@ -55,7 +63,6 @@ export const ProjectDetailsForm: FC = (props) => { if (project && projectId !== getValues("id")) { reset({ ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }); } @@ -109,14 +116,9 @@ export const ProjectDetailsForm: FC = (props) => { identifier: formData.identifier, description: formData.description, cover_image: formData.cover_image, + logo_props: formData.logo_props, }; - if (typeof formData.emoji_and_icon === "object") { - payload.emoji = null; - payload.icon_prop = formData.emoji_and_icon; - } else { - payload.emoji = formData.emoji_and_icon; - payload.icon_prop = null; - } + if (project.identifier !== formData.identifier) await projectService .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") @@ -139,20 +141,37 @@ export const ProjectDetailsForm: FC = (props) => {
-
- ( - - )} - /> -
+ ( + + + + } + onChange={(val) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + disabled={!isAdmin} + /> + )} + />
{watch("name")} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 42e310edb05..27f3eda3303 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -14,6 +14,7 @@ export * from "./sidebar-list"; export * from "./integration-card"; export * from "./member-list"; export * from "./member-list-item"; +export * from "./project-logo"; export * from "./project-settings-member-defaults"; export * from "./send-project-invitation-modal"; export * from "./confirm-project-member-remove"; diff --git a/web/components/project/project-logo.tsx b/web/components/project/project-logo.tsx new file mode 100644 index 00000000000..3d5887b2869 --- /dev/null +++ b/web/components/project/project-logo.tsx @@ -0,0 +1,34 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectLogoProps } from "@plane/types"; + +type Props = { + className?: string; + logo: TProjectLogoProps; +}; + +export const ProjectLogo: React.FC = (props) => { + const { className, logo } = props; + + if (logo.in_use === "icon" && logo.icon) + return ( + + {logo.icon.name} + + ); + + if (logo.in_use === "emoji" && logo.emoji) + return ( + + {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} + + ); + + return ; +}; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index c86ba0dc22b..11ed01cd314 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -29,10 +29,9 @@ import { LayersIcon, setPromiseToast, } from "@plane/ui"; -import { LeaveProjectModal, PublishProjectModal } from "components/project"; +import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "components/project"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; import { getNumberCount } from "helpers/string.helper"; // hooks import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; @@ -100,22 +99,20 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); + // refs + const actionSectionRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId: URLProjectId } = router.query; // derived values const project = getProjectById(projectId); - - const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; - const isViewerOrGuest = - project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); - const isCollapsed = themeStore.sidebarCollapsed; - - const actionSectionRef = useRef(null); - const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined; const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; + // auth + const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; + const isViewerOrGuest = + project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; @@ -178,9 +175,13 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { {({ open }) => ( <>
{provided && !disableDrag && ( = observer((props) => { >
} - className={`hidden flex-shrink-0 group-hover:block ${isMenuActive ? "!block" : ""}`} + className={cn("hidden flex-shrink-0 group-hover:block", { + "!block": isMenuActive, + })} buttonClassName="!text-custom-sidebar-text-400" ellipsis placement="bottom-start" diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 5ff95027b52..1fb746f517f 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -51,3 +51,12 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: return groupedReactions; }; + +export const convertHexEmojiToDecimal = (emojiUnified: string): string => { + if (!emojiUnified) return ""; + + return emojiUnified + .split("-") + .map((e) => parseInt(e, 16)) + .join("-"); +}; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index 8d78964ee70..441c14a42b4 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -43,3 +43,6 @@ export const orderJoinedProjects = ( return updatedSortOrder; }; + +export const projectIdentifierSanitizer = (identifier: string): string => + identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); diff --git a/web/layouts/settings-layout/project/sidebar.tsx b/web/layouts/settings-layout/project/sidebar.tsx index 8cf2befc279..628c2a854af 100644 --- a/web/layouts/settings-layout/project/sidebar.tsx +++ b/web/layouts/settings-layout/project/sidebar.tsx @@ -24,8 +24,8 @@ export const ProjectSettingsSidebar = () => {
SETTINGS - {[...Array(8)].map(() => ( - + {[...Array(8)].map((index) => ( + ))}
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index bc82302569b..48cd5a80c94 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -6,6 +6,7 @@ import { ThemeProvider } from "next-themes"; import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; +import "styles/emoji.css"; import "styles/react-day-picker.css"; // constants import { THEMES } from "constants/themes"; diff --git a/web/styles/emoji.css b/web/styles/emoji.css new file mode 100644 index 00000000000..2fe3ddab320 --- /dev/null +++ b/web/styles/emoji.css @@ -0,0 +1,52 @@ +.EmojiPickerReact { + --epr-category-navigation-button-size: 1.25rem !important; + --epr-category-label-height: 1.5rem !important; + --epr-emoji-size: 1.25rem !important; + --epr-picker-border-radius: 0.25rem !important; + --epr-horizontal-padding: 0.5rem !important; + --epr-emoji-padding: 0.5rem !important; + background-color: rgba(var(--color-background-100)) !important; +} + +.epr-main { + border: none !important; + border-radius: 0 !important; +} + +.epr-emoji-category-label { + font-size: 0.7875rem !important; + color: rgba(var(--color-text-300)) !important; + background-color: rgba(var(--color-background-100), 0.8) !important; +} + +.epr-category-nav, +.epr-header-overlay { + padding: 0.5rem !important; +} + +button.epr-emoji:hover > *, +button.epr-emoji:focus > * { + background-color: rgba(var(--color-background-80)) !important; +} + +input.epr-search { + font-size: 0.7875rem !important; + height: 2rem !important; + background: transparent !important; + border-color: rgba(var(--color-border-200)) !important; + border-radius: 0.25rem !important; +} + +input.epr-search::placeholder { + color: rgba(var(--color-text-400)) !important; +} + +button.epr-btn-clear-search:hover { + background-color: rgba(var(--color-background-80)) !important; + color: rgba(var(--color-text-300)) !important; +} + +.epr-emoji-variation-picker { + background-color: rgba(var(--color-background-100)) !important; + border-color: rgba(var(--color-border-200)) !important; +} diff --git a/yarn.lock b/yarn.lock index 9518afff6f8..c8cfcffd4af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2722,7 +2722,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.2.42": +"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== @@ -4064,6 +4064,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb" integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg== +emoji-picker-react@^4.5.16: + version "4.5.16" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.16.tgz#12111f89a7fd2bd74965337d53806f4153d65dc6" + integrity sha512-RXaOH1EapmqbtRSMaHnwJWMfA6kiPipg/gN4cFOQRQKvrTQIA3K5+yUyzFuq8O7umIEtXUi1C1tf2dPvyyn44Q== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" From c08d6987d09bd0dfeeb38a91e7998712bcd065bb Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:17:00 +0530 Subject: [PATCH 022/214] [WEB-658] fix: multiple toast alerts on adding existing issues (#3889) * fix: enums export in the types package * chore: remove NestedKeyOf type * fix: multiple toast alerts on adding existing issues * chore: added success toast alerts --- .../modals/existing-issues-list-modal.tsx | 6 ----- .../issue-layouts/empty-states/cycle.tsx | 23 +++++++++++++------ .../issue-layouts/empty-states/module.tsx | 7 ++++++ .../kanban/headers/group-by-card.tsx | 8 ++++++- .../list/headers/group-by-card.tsx | 8 ++++++- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index c064b02becf..3e3c2871c80 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -66,12 +66,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false)); handleClose(); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success", - message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, - }); }; useEffect(() => { diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 7b86c16a5a8..7f8c318c762 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -59,13 +59,22 @@ export const CycleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); + await issues + .addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }) + ) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }) + ); }; const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index c52d17af54c..c1709933566 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -59,6 +59,13 @@ export const ModuleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); await issues .addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the module successfully.", + }) + ) .catch(() => setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 41dda4b2137..b3cc24f289c 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -65,7 +65,13 @@ export const HeaderGroupByCard: FC = observer((props) => { const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 7edf89bf1a3..acf26adb5f8 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -47,7 +47,13 @@ export const HeaderGroupByCard = observer( const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { setToast({ type: TOAST_TYPE.ERROR, From 66f2492e606c384ec919f19e30f8179b48038af8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:17:50 +0530 Subject: [PATCH 023/214] [WEB-655] chore: peek overview dropdowns keyboard navigation improvement (#3888) * fix: enums export in the types package * chore: remove NestedKeyOf type * chore: peek overview keyboard accessibility improvement --------- Co-authored-by: Aaryan Khandelwal --- web/components/issues/peek-overview/view.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index aa7bd395fd5..c3ac1495aec 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -58,7 +58,9 @@ export const IssueView: FC = observer((props) => { } }); const handleKeyDown = () => { - if (!isAnyModalOpen) { + const slashCommandDropdownElement = document.querySelector("#slash-command"); + const dropdownElement = document.activeElement?.tagName === "INPUT"; + if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement) { removeRoutePeekId(); const issueElement = document.getElementById(`issue-${issueId}`); if (issueElement) issueElement?.focus(); From dd579f83ee79460fd684d151379e2541e2ea3b80 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Wed, 6 Mar 2024 19:27:04 +0530 Subject: [PATCH 024/214] chore: enabled module and cycle display properties in module and cycle issues (#3885) --- .../display-filters/display-properties.tsx | 49 ++++++---------- .../properties/all-properties.tsx | 58 +++++++++---------- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 871bf8ff5f1..d00848acd4f 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -1,6 +1,5 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // components import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; import { IIssueDisplayProperties } from "@plane/types"; @@ -14,19 +13,10 @@ type Props = { }; export const FilterDisplayProperties: React.FC = observer((props) => { - const router = useRouter(); - const { moduleId, cycleId } = router.query; const { displayProperties, handleUpdate } = props; const [previewEnabled, setPreviewEnabled] = React.useState(true); - const handleDisplayPropertyVisibility = (key: keyof IIssueDisplayProperties): boolean => { - const visibility = true; - if (key === "modules" && moduleId) return false; - if (key === "cycle" && cycleId) return false; - return visibility; - }; - return ( <> = observer((props) => { /> {previewEnabled && (
- {ISSUE_DISPLAY_PROPERTIES.map( - (displayProperty) => - handleDisplayPropertyVisibility(displayProperty?.key) && ( - - ) - )} + {ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => ( + + ))}
)} diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 8b3e2e67335..c3a6bc03766 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -52,7 +52,7 @@ export const IssueProperties: React.FC = observer((props) => { const { getStateById } = useProjectState(); // router const router = useRouter(); - const { workspaceSlug, cycleId, moduleId } = router.query; + const { workspaceSlug } = router.query; const currentLayout = `${activeLayout} layout`; // derived values const stateDetails = getStateById(issue.state_id); @@ -328,38 +328,34 @@ export const IssueProperties: React.FC = observer((props) => { {/* modules */} - {moduleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* cycles */} - {cycleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* estimates */} {areEstimatesEnabledForCurrentProject && ( From f188c9fdc5c98ec74ead2799b5872d53deca90ee Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 19:52:40 +0530 Subject: [PATCH 025/214] fix: build issues --- .../account/o-auth/o-auth-options.tsx | 10 +- .../widgets/recent-collaborators.tsx | 94 ------------------- web/components/instance/ai-form.tsx | 1 + web/components/instance/email-form.tsx | 1 + web/components/instance/general-form.tsx | 1 + .../instance/github-config-form.tsx | 1 + .../instance/google-config-form.tsx | 1 + web/components/instance/image-config-form.tsx | 1 + .../instance/setup-form/sign-in-form.tsx | 2 + web/components/integration/github/root.tsx | 1 + web/components/issues/delete-issue-modal.tsx | 2 + .../calendar/base-calendar-root.tsx | 2 + .../gantt/quick-add-issue-form.tsx | 3 +- .../issue-layouts/kanban/base-kanban-root.tsx | 2 +- web/components/modules/modal.tsx | 2 +- web/components/onboarding/invite-members.tsx | 4 +- .../project/create-project-modal.tsx | 2 +- .../project/delete-project-modal.tsx | 1 + web/components/project/integration-card.tsx | 3 + .../project/send-project-invitation-modal.tsx | 3 + web/components/toast-alert/index.tsx | 61 ------------ web/components/ui/graphs/marimekko-graph.tsx | 48 ---------- web/components/ui/multi-level-dropdown.tsx | 14 +-- .../workspace/create-workspace-form.tsx | 1 + .../workspace/sidebar-quick-action.tsx | 2 +- web/constants/dashboard.ts | 2 +- .../settings-layout/profile/sidebar.tsx | 2 - web/layouts/user-profile-layout/layout.tsx | 5 +- .../profile/[userId]/index.tsx | 2 +- web/pages/create-workspace.tsx | 2 +- 30 files changed, 47 insertions(+), 229 deletions(-) delete mode 100644 web/components/dashboard/widgets/recent-collaborators.tsx delete mode 100644 web/components/toast-alert/index.tsx delete mode 100644 web/components/ui/graphs/marimekko-graph.tsx diff --git a/web/components/account/o-auth/o-auth-options.tsx b/web/components/account/o-auth/o-auth-options.tsx index 1671b94fcfa..39123328eb3 100644 --- a/web/components/account/o-auth/o-auth-options.tsx +++ b/web/components/account/o-auth/o-auth-options.tsx @@ -1,10 +1,12 @@ import { observer } from "mobx-react-lite"; -// services +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; +// components import { GitHubSignInButton, GoogleSignInButton } from "components/account"; +// hooks import { useApplication } from "hooks/store"; -// ui -// components +// services +import { AuthService } from "services/auth.service"; type Props = { handleSignInRedirection: () => Promise; @@ -74,7 +76,7 @@ export const OAuthOptions: React.FC = observer((props) => {
{envConfig?.google_client_id && ( -
+
)} diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx deleted file mode 100644 index 438f87c4584..00000000000 --- a/web/components/dashboard/widgets/recent-collaborators.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react-lite"; -import Link from "next/link"; -// hooks -import { Avatar } from "@plane/ui"; -import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; -import { useDashboard, useMember, useUser } from "hooks/store"; -// components -// ui -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails) return null; - - return ( - -
- -
-
- {isCurrentUser ? "You" : userDetails?.display_name} -
-

- {issueCount} active issue{issueCount > 1 ? "s" : ""} -

- - ); -}); - -export const RecentCollaboratorsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
-
-

Most active members

-

- Top eight active members in your project by last activity -

-
- {widgetStats.length > 1 ? ( -
- {widgetStats.map((user) => ( - - ))} -
- ) : ( -
- -
- )} -
- ); -}); diff --git a/web/components/instance/ai-form.tsx b/web/components/instance/ai-form.tsx index 63246ff3407..250feb511c6 100644 --- a/web/components/instance/ai-form.tsx +++ b/web/components/instance/ai-form.tsx @@ -4,6 +4,7 @@ import { Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/email-form.tsx b/web/components/instance/email-form.tsx index 0caf825b662..664b96ea22e 100644 --- a/web/components/instance/email-form.tsx +++ b/web/components/instance/email-form.tsx @@ -4,6 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/general-form.tsx b/web/components/instance/general-form.tsx index cef757ccf16..6fedc88313e 100644 --- a/web/components/instance/general-form.tsx +++ b/web/components/instance/general-form.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IInstance, IInstanceAdmin } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/github-config-form.tsx b/web/components/instance/github-config-form.tsx index 90c4c880e87..20fb08aff58 100644 --- a/web/components/instance/github-config-form.tsx +++ b/web/components/instance/github-config-form.tsx @@ -4,6 +4,7 @@ import { Copy, Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/google-config-form.tsx b/web/components/instance/google-config-form.tsx index 49dfcc01cd2..27d4f43007b 100644 --- a/web/components/instance/google-config-form.tsx +++ b/web/components/instance/google-config-form.tsx @@ -4,6 +4,7 @@ import { Copy } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/image-config-form.tsx b/web/components/instance/image-config-form.tsx index 9ab79aad001..7be2089f132 100644 --- a/web/components/instance/image-config-form.tsx +++ b/web/components/instance/image-config-form.tsx @@ -4,6 +4,7 @@ import { Eye, EyeOff } from "lucide-react"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // types +import { IFormattedInstanceConfiguration } from "@plane/types"; // hooks import { useApplication } from "hooks/store"; diff --git a/web/components/instance/setup-form/sign-in-form.tsx b/web/components/instance/setup-form/sign-in-form.tsx index 106f2d6928f..a2e71faf298 100644 --- a/web/components/instance/setup-form/sign-in-form.tsx +++ b/web/components/instance/setup-form/sign-in-form.tsx @@ -5,6 +5,8 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; import { Input, Button, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; +// hooks +import { useUser } from "hooks/store"; // services import { AuthService } from "services/auth.service"; const authService = new AuthService(); diff --git a/web/components/integration/github/root.tsx b/web/components/integration/github/root.tsx index 956640de897..74f2f9c668b 100644 --- a/web/components/integration/github/root.tsx +++ b/web/components/integration/github/root.tsx @@ -30,6 +30,7 @@ import { IntegrationService, GithubIntegrationService } from "services/integrati // types import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types"; // fetch-keys +import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm"; export interface IIntegrationData { diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index ada126ccbe1..b6c08c14be9 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -5,6 +5,8 @@ import { AlertTriangle } from "lucide-react"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types import { TIssue } from "@plane/types"; +// hooks +import { useIssues, useProject } from "hooks/store"; type Props = { isOpen: boolean; diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 4162afe8583..2a8cbcc26e0 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -15,6 +15,8 @@ import { TGroupedIssues, TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; import { EIssueActions } from "../types"; import { handleDragDrop } from "./utils"; +import { useIssues, useUser } from "hooks/store"; +import { EUserProjectRoles } from "constants/project"; interface IBaseCalendarRoot { issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 94a6243e54f..b2d3ac9d463 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -15,6 +15,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui // types import { IProject, TIssue } from "@plane/types"; +import { ISSUE_CREATED } from "constants/event-tracker"; // constants interface IInputProps { @@ -162,7 +163,7 @@ export const GanttQuickAddIssueForm: React.FC = observe ) : ( -
-
-
-
- {alert.type === "success" ? ( -
-
-

{alert.title}

- {alert.message &&

{alert.message}

} -
-
-
-
- ))} -
- ); -}; - -export default ToastAlerts; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx deleted file mode 100644 index c0e6eb300e8..00000000000 --- a/web/components/ui/graphs/marimekko-graph.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// nivo -import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; -// helpers -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; -import { generateYAxisTickValues } from "helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants - -type Props = { - id: string; - value: string; - customYAxisTickValues?: number[]; -}; - -export const MarimekkoGraph: React.FC, "height" | "width">> = ({ - id, - value, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - {...rest} - /> -
-); diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 8bb0ebcf3a0..8633d1586e4 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -71,7 +71,7 @@ export const MultiLevelDropdown: React.FC = ({
{ + onClick={(e: any) => { if (option.hasChildren) { e?.stopPropagation(); e?.preventDefault(); @@ -108,12 +108,12 @@ export const MultiLevelDropdown: React.FC = ({ height === "sm" ? "max-h-28" : height === "md" - ? "max-h-44" - : height === "rg" - ? "max-h-56" - : height === "lg" - ? "max-h-80" - : "" + ? "max-h-44" + : height === "rg" + ? "max-h-56" + : height === "lg" + ? "max-h-80" + : "" }`} > {option.children ? ( diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index 822ee1347f5..9cbfa25a384 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -12,6 +12,7 @@ import { useEventTracker, useWorkspace } from "hooks/store"; // ui // types import { IWorkspace } from "@plane/types"; +import { WorkspaceService } from "services/workspace.service"; type Props = { onSubmit?: (res: IWorkspace) => Promise; diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index d2ce2f5b34b..a378a8b3f5f 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -27,7 +27,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); - const { storedValue } = useLocalStorage>>("draftedIssue", {}); + const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 35599d661c8..3d11b4f123c 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -11,7 +11,7 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; -import { Props } from "components/icons/types"; + // constants import { EUserWorkspaceRoles } from "./workspace"; // icons diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index caa5cd56e1e..1bae51d8b5c 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -11,9 +11,7 @@ import { useApplication, useUser, useWorkspace } from "hooks/store"; import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; -import { useApplication, useUser, useWorkspace } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useToast from "hooks/use-toast"; const WORKSPACE_ACTION_LINKS = [ { diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 243eaed1a36..fcabf5f4957 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,10 +1,9 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -// hooks -import { ProfileNavbar, ProfileSidebar } from "components/profile"; -import { useUser } from "hooks/store"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; +// hooks +import { useUser } from "hooks/store"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index eb71989ed4f..947da136974 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -49,7 +49,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
- +
diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 629e4a379a5..add8d66731a 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -66,7 +66,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
From 6a6ab5544ad0b1e25143bed64f781bacb9da03d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:01:14 +0530 Subject: [PATCH 026/214] fix: build issues --- space/components/issues/peek-overview/comment/add-comment.tsx | 2 +- .../issues/peek-overview/comment/comment-detail-card.tsx | 2 +- space/components/issues/peek-overview/issue-properties.tsx | 2 +- space/components/issues/peek-overview/layout.tsx | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index ef1a115d282..3dba8b29ca8 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -93,7 +93,7 @@ export const AddComment: React.FC = observer((props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} submitButton={ diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 7c6abe19956..c3a26f83e09 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -115,7 +115,7 @@ export const CommentCard: React.FC = observer((props) => { value={value} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} /> diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index a6dcedf0804..0c327ca590f 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -94,7 +94,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod > {priority && ( - + )} {priority?.title ?? "None"} diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 5a4144db392..7345b4b28f9 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -11,9 +11,8 @@ import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overvie // lib import { useMobxStore } from "lib/mobx/store-provider"; -type Props = {}; -export const IssuePeekOverview: React.FC = observer(() => { +export const IssuePeekOverview: React.FC = observer(() => { // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); From 4861be2773327319a3ba35c61c3d020724cdd540 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:15:42 +0530 Subject: [PATCH 027/214] fix: adding missing deps --- space/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/space/package.json b/space/package.json index 7018cd24165..4951d5e301a 100644 --- a/space/package.json +++ b/space/package.json @@ -20,6 +20,7 @@ "@plane/document-editor": "*", "@plane/lite-text-editor": "*", "@plane/rich-text-editor": "*", + "@plane/types": "*", "@plane/ui": "*", "@sentry/nextjs": "^7.85.0", "axios": "^1.3.4", From a08f401452eaf208778cd3415a00601393542648 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:16:54 +0530 Subject: [PATCH 028/214] [WEB-630] refactor: empty state (#3858) * refactor: empty state global config file added and empty state component refactor * refactor: empty state component refactor * chore: empty state refactor * chore: empty state config file updated * chore: empty state action button permission logic updated * chore: empty state config file updated * chore: cycle and module empty filter state updated * chore: empty state asset updated * chore: empty state config file updated * chore: empty state config file updated * chore: empty state component improvement * chore: empty state action button improvement * fix: merge conflict --- .../cycles/active-cycle-details.tsx | 25 +- web/components/cycles/cycles-board.tsx | 23 +- web/components/cycles/cycles-list.tsx | 25 +- web/components/empty-state/empty-state.tsx | 237 ++++---- web/components/estimates/estimates-list.tsx | 27 +- web/components/exporter/guide.tsx | 31 +- web/components/integration/guide.tsx | 31 +- .../empty-states/archived-issues.tsx | 65 +-- .../issue-layouts/empty-states/cycle.tsx | 86 +-- .../empty-states/draft-issues.tsx | 61 +- .../issue-layouts/empty-states/module.tsx | 83 +-- .../empty-states/project-issues.tsx | 80 +-- .../roots/all-issue-layout-root.tsx | 59 +- .../labels/project-setting-label-list.tsx | 33 +- web/components/modules/modules-list-view.tsx | 41 +- .../page-views/workspace-dashboard.tsx | 42 +- web/components/pages/pages-list/list-view.tsx | 42 +- .../pages/pages-list/recent-pages-list.tsx | 36 +- web/components/profile/profile-issues.tsx | 31 +- web/components/project/card-list.tsx | 39 +- web/components/views/views-list.tsx | 39 +- web/constants/empty-state.ts | 528 +++++++++++------- web/pages/[workspaceSlug]/analytics.tsx | 42 +- .../projects/[projectId]/cycles/index.tsx | 48 +- .../projects/[projectId]/pages/index.tsx | 46 +- .../[projectId]/settings/integrations.tsx | 33 +- .../[workspaceSlug]/settings/api-tokens.tsx | 32 +- .../settings/webhooks/index.tsx | 29 +- ...k-resp.webp => gantt_chart-dark-resp.webp} | Bin ...{gantt-dark.webp => gantt_chart-dark.webp} | Bin ...-resp.webp => gantt_chart-light-resp.webp} | Bin ...antt-light.webp => gantt_chart-light.webp} | Bin 32 files changed, 749 insertions(+), 1145 deletions(-) rename web/public/empty-state/module-issues/{gantt-dark-resp.webp => gantt_chart-dark-resp.webp} (100%) rename web/public/empty-state/module-issues/{gantt-dark.webp => gantt_chart-dark.webp} (100%) rename web/public/empty-state/module-issues/{gantt-light-resp.webp => gantt_chart-light-resp.webp} (100%) rename web/public/empty-state/module-issues/{gantt-light.webp => gantt_chart-light.webp} (100%) diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index d9309d4b529..a6457ab3c14 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,10 +1,9 @@ import { MouseEvent } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; +import { useCycle, useIssues, useMember, useProject } from "hooks/store"; // ui import { SingleProgressStats } from "components/core"; import { @@ -23,7 +22,7 @@ import { import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; import { StateDropdown } from "components/dropdowns"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // icons import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers @@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types"; import { EIssuesStoreType } from "constants/issue"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; interface IActiveCycleDetails { workspaceSlug: string; @@ -45,9 +44,6 @@ interface IActiveCycleDetails { export const ActiveCycleDetails: React.FC = observer((props) => { // props const { workspaceSlug, projectId } = props; - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); const { issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); @@ -78,11 +74,6 @@ export const ActiveCycleDetails: React.FC = observer((props : null ); - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode); - if (!activeCycle && isLoading) return ( @@ -90,15 +81,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); - if (!activeCycle) - return ( - - ); + if (!activeCycle) return ; const endDate = new Date(activeCycle.end_date ?? ""); const startDate = new Date(activeCycle.start_date ?? ""); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 00c98e57cb2..278d55071b5 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,13 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesBoard { cycleIds: string[]; @@ -19,15 +16,6 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -52,12 +40,7 @@ export const CyclesBoard: FC = observer((props) => {
) : ( - + )} ); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 99cf1f2b1f0..f6ad64f990a 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,15 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components -import { Loader } from "@plane/ui"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesList { cycleIds: string[]; @@ -20,15 +17,6 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -54,12 +42,7 @@ export const CyclesList: FC = observer((props) => {
) : ( - + )} ) : ( diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 9d77a81d0cd..9ef216068c7 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,119 +1,150 @@ import React from "react"; +import Link from "next/link"; import Image from "next/image"; + +import { useTheme } from "next-themes"; +// hooks +import { useUser } from "hooks/store"; // components -// ui -import { Button, getButtonStyling } from "@plane/ui"; -// helper -import { cn } from "helpers/common.helper"; +import { Button, TButtonVariant } from "@plane/ui"; import { ComicBoxButton } from "./comic-box-button"; +// constant +import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; +// helpers +import { cn } from "helpers/common.helper"; -type Props = { - title: string; - description?: string; - image: any; - primaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - secondaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - comicBox?: { - title: string; - description: string; - }; - size?: "sm" | "lg"; - disabled?: boolean; +export type EmptyStateProps = { + type: EmptyStateKeys; + size?: "sm" | "md" | "lg"; + layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + additionalPath?: string; + primaryButtonOnClick?: () => void; + primaryButtonLink?: string; + secondaryButtonOnClick?: () => void; }; -export const EmptyState: React.FC = ({ - title, - description, - image, - primaryButton, - secondaryButton, - comicBox, - size = "sm", - disabled = false, -}) => { - const emptyStateHeader = ( - <> - {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

- )} - - ); +export const EmptyState: React.FC = (props) => { + const { + type, + size = "lg", + layout = "screen-detailed", + additionalPath = "", + primaryButtonOnClick, + primaryButtonLink, + secondaryButtonOnClick, + } = props; + // store + const { + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); + // theme + const { resolvedTheme } = useTheme(); + // current empty state details + const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = + EMPTY_STATE_DETAILS[type]; + // resolved empty state path + const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ + resolvedTheme === "light" ? "light" : "dark" + }.webp`; + // current access type + const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole; + // permission + const isEditingAllowed = currentAccessType && access && currentAccessType >= access; + const anyButton = primaryButton || secondaryButton; - const secondaryButtonElement = secondaryButton && ( - - ); + // primary button + const renderPrimaryButton = () => { + if (!primaryButton) return null; - return ( -
-
-
{emptyStateHeader}
+ const commonProps = { + size: size, + variant: "primary" as TButtonVariant, + prependIcon: primaryButton.icon, + onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, + disabled: !isEditingAllowed, + }; - {primaryButton?.text + ); + } else if (primaryButtonLink) { + return ( + + + + ); + } else { + return ; + } + }; + // secondary button + const renderSecondaryButton = () => { + if (!secondaryButton) return null; -
- {primaryButton && ( - <> -
- {comicBox ? ( - primaryButton.onClick()} - disabled={disabled} - /> - ) : ( -
primaryButton.onClick()} - > - {primaryButton.icon} - {primaryButton.text} -
- )} -
- - )} + return ( + + ); + }; - {secondaryButton && secondaryButtonElement} + return ( + <> + {layout === "screen-detailed" && ( +
+
+
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ + {path && ( + {key + )} + + {anyButton && ( + <> +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
+ + )} +
-
-
+ )} + ); }; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 8e447d6ac5b..1769ba01699 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,20 +1,19 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // store hooks -import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { orderArrayBy } from "helpers/array.helper"; -import { useEstimate, useProject, useUser } from "hooks/store"; +import { useEstimate, useProject } from "hooks/store"; // components +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers +import { orderArrayBy } from "helpers/array.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states @@ -24,12 +23,9 @@ export const EstimatesList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); - const { currentUser } = useUser(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -55,10 +51,6 @@ export const EstimatesList: React.FC = observer(() => { }); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode); - return ( <> { ) : (
- +
) ) : ( diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index 381b168bd45..03d925b62a6 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -4,26 +4,24 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { Exporter, SingleExport } from "components/exporter"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; -import { EXPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { Exporter, SingleExport } from "components/exporter"; +import { ImportExportSettingsLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons -// fetch-keys +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // constants +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +import { EXPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -36,8 +34,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -50,10 +46,6 @@ const IntegrationGuide = observer(() => { : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode); - const handleCsvClose = () => { router.replace(`/${workspaceSlug?.toString()}/settings/exports`); }; @@ -149,12 +141,7 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index a75c71f1f0c..84d422d1219 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -3,28 +3,26 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; -import { IMPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { ImportExportSettingsLoader } from "components/ui"; +import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons +import { RefreshCw } from "lucide-react"; // types import { IImporterService } from "@plane/types"; -// fetch-keys // constants +import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; +import { IMPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -37,8 +35,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -49,10 +45,6 @@ const IntegrationGuide = observer(() => { workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode); - const handleDeleteImport = (importService: IImporterService) => { setImportToDelete(importService); setDeleteImportModal(true); @@ -145,12 +137,7 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 96887ed6062..c9de2279c89 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,49 +1,27 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectArchivedEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,33 +39,20 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["archived"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["archived"].title, - description: EMPTY_ISSUE_STATE_DETAILS["archived"].description, - image: EmptyStateImagePath, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text, - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; return (
- + 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 7f8c318c762..350e4dbb47a 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,21 +1,17 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -// ui -// components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -26,33 +22,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -77,43 +56,9 @@ export const CycleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setCycleIssuesListModal(true), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; + const additionalPath = activeLayout ?? "list"; + const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( <> @@ -126,7 +71,20 @@ export const CycleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToCycle} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 77b1123b615..0968ed07afe 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,49 +1,26 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectDraftEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,29 +38,19 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["draft"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["draft"].title, - description: EMPTY_ISSUE_STATE_DETAILS["draft"].description, - image: EmptyStateImagePath, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index c1709933566..6c0cd0cd6f1 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,20 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -import { ExistingIssuesListModal } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // ui // components +import { ExistingIssuesListModal } from "components/core"; +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -25,33 +23,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.MODULE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -75,42 +56,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setModuleIssuesListModal(true), - }, - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; + const additionalPath = activeLayout ?? "list"; return ( <> @@ -123,7 +70,19 @@ export const ModuleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToModule} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index e44dd562686..12642d364d6 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,51 +1,29 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -63,40 +41,26 @@ export const ProjectEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["project"].title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].description, - image: EmptyStateImagePath, - comicBox: { - title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description, - }, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text, - onClick: () => { - setTrackElement("Project issue empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - }, - size: "lg", - disabled: !isEditingAllowed, - }; + const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 + ? undefined + : () => { + setTrackElement("Project issue empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + } + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 84101542fa3..1367eccc481 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -2,32 +2,28 @@ import React, { Fragment, useCallback, useMemo } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +// components import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; +import { EmptyState } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; -import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; -import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; -// components // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; // constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, globalViewId, ...routeFilters } = router.query; - // theme - const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); // store @@ -39,8 +35,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { dataViewId, issueIds } = groupedIssueIds; const { - membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, - currentUser, + membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); const { workspaceProjectIds } = useProject(); @@ -48,10 +43,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; - const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode); // filter init from the query params @@ -185,46 +176,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { [canEditProperties, handleIssues] ); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { return ; } + const emptyStateType = + (workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; + return (
{issueIds.length === 0 ? ( 0 ? currentViewDetails.title : "No project"} - description={ - (workspaceProjectIds ?? []).length > 0 - ? currentViewDetails.description - : "To create issues or manage your work, you need to create a project or be a part of one." - } + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" - primaryButton={ + primaryButtonOnClick={ (workspaceProjectIds ?? []).length > 0 ? currentView !== "custom-view" && currentView !== "subscribed" - ? { - text: "Create new issue", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, + ? () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); } : undefined - : { - text: "Start your first project", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + : () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateProjectModal(true); } } - disabled={!isEditingAllowed} /> ) : ( diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index ba6b43b0bf1..1e83167ae6c 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,4 +1,6 @@ import React, { useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; import { DragDropContext, Draggable, @@ -7,26 +9,23 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; -import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Button, Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useLabel } from "hooks/store"; +import useDraggableInPortal from "hooks/use-draggable-portal"; +// components import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useLabel, useUser } from "hooks/store"; -import useDraggableInPortal from "hooks/use-draggable-portal"; -// components +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { currentUser } = useUser(); const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); // portal const renderDraggable = useDraggableInPortal(); @@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { setLabelForm(true); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode); - const onDragEnd = (result: DropResult) => { const { combine, draggableId, destination, source } = result; @@ -121,13 +113,8 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
- +
+
) : ( projectLabelsTree && ( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 33c11cbd88b..78b4a657107 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,40 +1,28 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useModule } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; +import { EmptyState } from "components/empty-state"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; +import { EmptyStateType } from "constants/empty-state"; export const ModulesListView: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { projectModuleIds, loader } = useModule(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (loader || !projectModuleIds) return ( <> @@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => { ) : ( { - setTrackElement("Module empty state"); - commandPaletteStore.toggleCreateModuleModal(true); - }, + type={EmptyStateType.PROJECT_MODULE} + primaryButtonOnClick={() => { + setTrackElement("Module empty state"); + commandPaletteStore.toggleCreateModuleModal(true); }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 2f8392bc2fd..f910625ca8a 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,41 +1,30 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { Spinner } from "@plane/ui"; import { DashboardWidgets } from "components/dashboard"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { IssuePeekOverview } from "components/issues"; import { TourRoot } from "components/onboarding"; import { UserGreetingsView } from "components/user"; // ui +import { Spinner } from "@plane/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const WorkspaceDashboardView = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { captureEvent, setTrackElement } = useEventTracker(); const { commandPalette: { toggleCreateProjectModal }, router: { workspaceSlug }, } = useApplication(); - const { - currentUser, - updateTourCompleted, - membership: { currentWorkspaceRole }, - } = useUser(); + const { currentUser, updateTourCompleted } = useUser(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode); - const handleTourCompleted = () => { updateTourCompleted() .then(() => { @@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug); }, [fetchHomeDashboardWidgets, workspaceSlug]); - const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - return ( <> {currentUser && !currentUser.is_tour_completed && ( @@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => { ) : ( { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }, - }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description, + type={EmptyStateType.WORKSPACE_DASHBOARD} + primaryButtonOnClick={() => { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index 0d468ef3c2f..8c1a09e73cf 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components +import { EmptyState } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui +import { Loader } from "@plane/ui"; // constants +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; @@ -19,34 +17,20 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreatePageModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); // local storage const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent"); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const currentPageTabDetails = pageTab - ? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS] - : PAGE_EMPTY_STATE_DETAILS["All"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode); - - const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites"; - // here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const emptyStateType = pageTab ? `project-page-${pageTab}` : EmptyStateType.PROJECT_PAGE_ALL; + const isButtonVisible = pageTab !== "archived" && pageTab !== "favorites"; return ( <> @@ -60,18 +44,8 @@ export const PagesListView: FC = (props) => { ) : ( toggleCreatePageModal(true), - } - : undefined - } - disabled={!isEditingAllowed} + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} + primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined} /> )}
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 28a4300312f..45de8db0db7 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,39 +1,27 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesListView } from "components/pages/pages-list"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components +import { PagesListView } from "components/pages/pages-list"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); - const { recentProjectPages } = useProjectPages(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode); + const { recentProjectPages } = useProjectPages(); // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (!recentProjectPages) { return ( @@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => { ) : ( <> commandPaletteStore.toggleCreatePageModal(true), - }} + type={EmptyStateType.PROJECT_PAGE_RECENT} + primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)} size="sm" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index b6a99baf94e..f94c1d91f95 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,18 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // hooks -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -28,13 +26,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { workspaceSlug: string; userId: string; }; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { issues: { loader, groupedIssueIds, fetchIssues, setViewId }, issuesFilter: { issueFilters, fetchFilters }, @@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const emptyStateType = `profile-${type}`; if (!groupedIssueIds || loader === "init-loader") return <>{activeLayout === "list" ? : }; if (groupedIssueIds.length === 0) { - return ( - - ); + return ; } return ( diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index a19b53fbb3d..df63dfb738a 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,31 +1,19 @@ import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectCardList = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); - const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); if (!workspaceProjectIds) return ; @@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
) : ( { - setTrackElement("Project empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, - }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description, + type={EmptyStateType.WORKSPACE_PROJECTS} + primaryButtonOnClick={() => { + setTrackElement("Project empty state"); + commandPaletteStore.toggleCreateProjectModal(true); }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 9d8bf85e6f7..ba4bef2b8e2 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,45 +1,32 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; import { Search } from "lucide-react"; // hooks +import { useApplication, useProjectView } from "hooks/store"; // components -import { Input } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// ui +import { EmptyState } from "components/empty-state"; import { ViewListLoader } from "components/ui"; import { ProjectViewListItem } from "components/views"; +// ui +import { Input } from "@plane/ui"; // constants -import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useProjectView, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectViewsList = observer(() => { // states const [query, setQuery] = useState(""); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateViewModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { projectViewIds, getViewById, loader } = useProjectView(); if (loader || !projectViewIds) return ; const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode); - const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - return ( <> {viewsList.length > 0 ? ( @@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
) : ( - toggleCreateViewModal(true), - }} - size="lg" - disabled={!isEditingAllowed} - /> + toggleCreateViewModal(true)} /> )} ); diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index a1b2b06f333..38f334b2013 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -1,366 +1,516 @@ -// workspace empty state -export const WORKSPACE_EMPTY_STATE_DETAILS = { - dashboard: { +import { EUserProjectRoles } from "./project"; +import { EUserWorkspaceRoles } from "./workspace"; + +export interface EmptyStateDetails { + key: string; + title?: string; + description?: string; + path?: string; + primaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + secondaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + accessType?: "workspace" | "project"; + access?: EUserWorkspaceRoles | EUserProjectRoles; +} + +export type EmptyStateKeys = keyof typeof emptyStateDetails; + +export enum EmptyStateType { + WORKSPACE_DASHBOARD = "workspace-dashboard", + WORKSPACE_ANALYTICS = "workspace-analytics", + WORKSPACE_PROJECTS = "workspace-projects", + WORKSPACE_ALL_ISSUES = "workspace-all-issues", + WORKSPACE_ASSIGNED = "workspace-assigned", + WORKSPACE_CREATED = "workspace-created", + WORKSPACE_SUBSCRIBED = "workspace-subscribed", + WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", + WORKSPACE_NO_PROJECTS = "workspace-no-projects", + WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", + WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", + WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", + WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", + PROFILE_ASSIGNED = "profile-assigned", + PROFILE_CREATED = "profile-created", + PROFILE_SUBSCRIBED = "profile-subscribed", + PROJECT_SETTINGS_LABELS = "project-settings-labels", + PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", + PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", + PROJECT_CYCLES = "project-cycles", + PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", + PROJECT_CYCLE_ACTIVE = "project-cycle-active", + PROJECT_CYCLE_UPCOMING = "project-cycle-upcoming", + PROJECT_CYCLE_COMPLETED = "project-cycle-completed", + PROJECT_CYCLE_DRAFT = "project-cycle-draft", + PROJECT_EMPTY_FILTER = "project-empty-filter", + PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", + PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", + PROJECT_NO_ISSUES = "project-no-issues", + PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", + PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", + VIEWS_EMPTY_SEARCH = "views-empty-search", + PROJECTS_EMPTY_SEARCH = "projects-empty-search", + COMMANDK_EMPTY_SEARCH = "commandK-empty-search", + MEMBERS_EMPTY_SEARCH = "members-empty-search", + PROJECT_MODULE_ISSUES = "project-module-issues", + PROJECT_MODULE = "project-module", + PROJECT_VIEW = "project-view", + PROJECT_PAGE = "project-page", + PROJECT_PAGE_ALL = "project-page-all", + PROJECT_PAGE_FAVORITE = "project-page-favorite", + PROJECT_PAGE_PRIVATE = "project-page-private", + PROJECT_PAGE_SHARED = "project-page-shared", + PROJECT_PAGE_ARCHIVED = "project-page-archived", + PROJECT_PAGE_RECENT = "project-page-recent", +} + +const emptyStateDetails = { + // workspace + "workspace-dashboard": { + key: "workspace-dashboard", title: "Overview of your projects, activity, and metrics", description: " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + path: "/empty-state/onboarding/dashboard", + // path: "/empty-state/onboarding/", primaryButton: { text: "Build your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, + + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - analytics: { + "workspace-analytics": { + key: "workspace-analytics", title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", description: "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + path: "/empty-state/onboarding/analytics", primaryButton: { text: "Create Cycles and Modules first", + comicBox: { + title: "Analytics works best with Cycles + Modules", + description: + "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", + }, }, - comicBox: { - title: "Analytics works best with Cycles + Modules", - description: - "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { + "workspace-projects": { + key: "workspace-projects", title: "Start a Project", description: "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.", + path: "/empty-state/onboarding/projects", primaryButton: { text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - "assigned-notification": { - key: "assigned-notification", - title: "No issues assigned", - description: "Updates for issues assigned to you can be seen here", + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "created-notification": { - key: "created-notification", - title: "No updates to issues", - description: "Updates to issues created by you can be seen here", - }, - "subscribed-notification": { - key: "subscribed-notification", - title: "No updates to issues", - description: "Updates to any issue you are subscribed to can be seen here", - }, -}; - -export const ALL_ISSUES_EMPTY_STATE_DETAILS = { - "all-issues": { - key: "all-issues", + // all-issues + "workspace-all-issues": { + key: "workspace-all-issues", title: "No issues in the project", description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + path: "/empty-state/all-issues/all-issues", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - assigned: { - key: "assigned", + "workspace-assigned": { + key: "workspace-assigned", title: "No issues yet", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/all-issues/assigned", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - created: { - key: "created", + "workspace-created": { + key: "workspace-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/all-issues/created", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - subscribed: { - key: "subscribed", + "workspace-subscribed": { + key: "workspace-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/all-issues/subscribed", }, - "custom-view": { - key: "custom-view", + "workspace-custom-view": { + key: "workspace-custom-view", title: "No issues yet", description: "Issues that applies to the filters, track all of them here.", + path: "/empty-state/all-issues/custom-view", }, -}; - -export const SEARCH_EMPTY_STATE_DETAILS = { - views: { - key: "views", - title: "No matching views", - description: "No views match the search criteria. Create a new view instead.", - }, - projects: { - key: "projects", - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - }, - commandK: { - key: "commandK", - title: "No results found. ", - }, - members: { - key: "members", - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", + "workspace-no-projects": { + key: "workspace-no-projects", + title: "No project", + description: "To create issues or manage your work, you need to create a project or be a part of one.", + path: "/empty-state/onboarding/projects", + primaryButton: { + text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, -}; - -export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = { - "api-tokens": { - key: "api-tokens", + // workspace settings + "workspace-settings-api-tokens": { + key: "workspace-settings-api-tokens", title: "No API tokens created", description: "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", + path: "/empty-state/workspace-settings/api-tokens", }, - webhooks: { - key: "webhooks", + "workspace-settings-webhooks": { + key: "workspace-settings-webhooks", title: "No webhooks added", description: "Create webhooks to receive real-time updates and automate actions.", + path: "/empty-state/workspace-settings/webhooks", }, - export: { - key: "export", + "workspace-settings-export": { + key: "workspace-settings-export", title: "No previous exports yet", description: "Anytime you export, you will also have a copy here for reference.", + path: "/empty-state/workspace-settings/exports", }, - import: { - key: "export", + "workspace-settings-import": { + key: "workspace-settings-import", title: "No previous imports yet", description: "Find all your previous imports here and download them.", + path: "/empty-state/workspace-settings/imports", }, -}; - -// profile empty state -export const PROFILE_EMPTY_STATE_DETAILS = { - assigned: { - key: "assigned", + // profile + "profile-assigned": { + key: "profile-assigned", title: "No issues are assigned to you", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/profile/assigned", }, - subscribed: { - key: "created", + "profile-created": { + key: "profile-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/profile/created", }, - created: { - key: "subscribed", + "profile-subscribed": { + key: "profile-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/profile/subscribed", }, -}; - -// project empty state - -export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = { - labels: { - key: "labels", + // project settings + "project-settings-labels": { + key: "project-settings-labels", title: "No labels yet", description: "Create labels to help organize and filter issues in you project.", + path: "/empty-state/project-settings/labels", }, - integrations: { - key: "integrations", + "project-settings-integrations": { + key: "project-settings-integrations", title: "No integrations configured", description: "Configure GitHub and other integrations to sync your project issues.", + path: "/empty-state/project-settings/integrations", }, - estimate: { - key: "estimate", + "project-settings-estimate": { + key: "project-settings-estimate", title: "No estimates added", description: "Create a set of estimates to communicate the amount of work per issue.", + path: "/empty-state/project-settings/estimates", }, -}; - -export const CYCLE_EMPTY_STATE_DETAILS = { - cycles: { + // project cycles + "project-cycles": { + key: "project-cycles", title: "Group and timebox your work in Cycles.", description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, + path: "/empty-state/onboarding/cycles", primaryButton: { text: "Set your first cycle", + comicBox: { + title: "Cycles are repetitive time-boxes.", + description: + "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", + }, }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "no-issues": { - key: "no-issues", + "project-cycle-no-issues": { + key: "project-cycle-no-issues", title: "No issues added to the cycle", description: "Add or create issues you wish to timebox and deliver within this cycle", + path: "/empty-state/cycle-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - active: { - key: "active", + "project-cycle-active": { + key: "project-cycle-active", title: "No active cycles", description: "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", + path: "/empty-state/cycle/active", }, - upcoming: { - key: "upcoming", + "project-cycle-upcoming": { + key: "project-cycle-upcoming", title: "No upcoming cycles", description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", + path: "/empty-state/cycle/upcoming", }, - completed: { - key: "completed", + "project-cycle-completed": { + key: "project-cycle-completed", title: "No completed cycles", description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", + path: "/empty-state/cycle/completed", }, - draft: { - key: "draft", + "project-cycle-draft": { + key: "project-cycle-draft", title: "No draft cycles", description: "No dates added in cycles? Find them here as drafts.", + path: "/empty-state/cycle/draft", }, -}; - -export const EMPTY_FILTER_STATE_DETAILS = { - archived: { - key: "archived", + // empty filters + "project-empty-filter": { + key: "project-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - draft: { - key: "draft", + "project-archived-empty-filter": { + key: "project-archived-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - project: { - key: "project", + "project-draft-empty-filter": { + key: "project-draft-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const EMPTY_ISSUE_STATE_DETAILS = { - archived: { - key: "archived", + // project issues + "project-no-issues": { + key: "project-no-issues", + title: "Create an issue and assign it to someone, even yourself", + description: + "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", + path: "/empty-state/onboarding/issues", + primaryButton: { + text: "Create your first issue", + comicBox: { + title: "Issues are building blocks in Plane.", + description: + "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", + }, + }, + accessType: "project", + access: EUserProjectRoles.MEMBER, + }, + "project-archived-no-issues": { + key: "project-archived-no-issues", title: "No archived issues yet", description: - "Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.", + "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", + path: "/empty-state/archived/empty-issues", primaryButton: { text: "Set automation", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - draft: { - key: "draft", + "project-draft-no-issues": { + key: "project-draft-no-issues", title: "No draft issues yet", description: "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", + path: "/empty-state/draft/draft-issues-empty", }, - project: { - key: "project", - title: "Create an issue and assign it to someone, even yourself", - description: - "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, - primaryButton: { - text: "Create your first issue", - }, + "views-empty-search": { + key: "views-empty-search", + title: "No matching views", + description: "No views match the search criteria. Create a new view instead.", + path: "/empty-state/search/search", }, -}; - -export const MODULE_EMPTY_STATE_DETAILS = { - "no-issues": { - key: "no-issues", + "projects-empty-search": { + key: "projects-empty-search", + title: "No matching projects", + description: "No projects detected with the matching criteria. Create a new project instead.", + path: "/empty-state/search/project", + }, + "commandK-empty-search": { + key: "commandK-empty-search", + title: "No results found. ", + path: "/empty-state/search/search", + }, + "members-empty-search": { + key: "members-empty-search", + title: "No matching members", + description: "Add them to the project if they are already a part of the workspace", + path: "/empty-state/search/member", + }, + // project module + "project-module-issues": { + key: "project-modules-issues", title: "No issues in the module", description: "Create or add issues which you want to accomplish as part of this module", + path: "/empty-state/module-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - modules: { + "project-module": { + key: "project-module", title: "Map your project milestones to Modules and track aggregated work easily.", description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, + path: "/empty-state/onboarding/modules", primaryButton: { text: "Build your first module", + comicBox: { + title: "Modules help group work by hierarchy.", + description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const VIEW_EMPTY_STATE_DETAILS = { - "project-views": { + // project views + "project-view": { + key: "project-view", title: "Save filtered views for your project. Create as many as you need", description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, + path: "/empty-state/onboarding/views", primaryButton: { text: "Create your first view", + comicBox: { + title: "Views work atop Issue properties.", + description: "You can create a view from here with as many properties as filters as you see fit.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const PAGE_EMPTY_STATE_DETAILS = { - pages: { + // project pages + "project-page": { key: "pages", title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", description: "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", + path: "/empty-state/onboarding/pages", primaryButton: { text: "Create your first page", + comicBox: { + title: "A page can be a doc or a doc of docs.", + description: + "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", + }, }, - comicBox: { - title: "A page can be a doc or a doc of docs.", - description: - "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", - }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - All: { - key: "all", + "project-page-all": { + key: "project-page-all", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", + path: "/empty-state/pages/all", }, - Favorites: { - key: "favorites", + "project-page-favorite": { + key: "project-page-favorite", title: "No favorite pages yet", description: "Favorites for quick access? mark them and find them right here.", + path: "/empty-state/pages/favorites", }, - Private: { - key: "private", + "project-page-private": { + key: "project-page-private", title: "No private pages yet", description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + path: "/empty-state/pages/private", }, - Shared: { - key: "shared", + "project-page-shared": { + key: "project-page-shared", title: "No shared pages yet", description: "See pages shared with everyone in your project right here.", + path: "/empty-state/pages/shared", }, - Archived: { - key: "archived", + "project-page-archived": { + key: "project-page-archived", title: "No archived pages yet", description: "Archive pages not on your radar. Access them here when needed.", + path: "/empty-state/pages/archived", }, - Recent: { - key: "recent", + "project-page-recent": { + key: "project-page-recent", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", + path: "/empty-state/pages/recent", primaryButton: { text: "Create new page", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; +} as const; + +export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 658f3e34c4e..c7ee67cab8f 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,44 +1,33 @@ import React, { Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks +import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store"; // layouts +import { AppLayout } from "layouts/app-layout"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { WorkspaceAnalyticsHeader } from "components/headers"; -// constants -import { ANALYTICS_TABS } from "constants/analytics"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; -import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; +// constants +import { ANALYTICS_TABS } from "constants/analytics"; +import { EmptyStateType } from "constants/empty-state"; const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { analytics_tab } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { workspaceProjectIds } = useProject(); const { currentWorkspace } = useWorkspace(); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; return ( @@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
) : ( { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }, - }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description, + type={EmptyStateType.WORKSPACE_ANALYTICS} + primaryButtonOnClick={() => { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index ac2b760ef11..a22e252f27b 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,39 +1,31 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks -import { Tooltip } from "@plane/ui"; -import { PageHead } from "components/core"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CyclesHeader } from "components/headers"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; +import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; +import { CyclesHeader } from "components/headers"; +import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { EmptyState } from "components/empty-state"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // ui +import { Tooltip } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { TCycleView, TCycleLayout } from "@plane/types"; // constants +import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EmptyStateType } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById } = useProject(); // router @@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); const totalCycles = currentProjectCycleIds?.length ?? 0; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; @@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { {totalCycles === 0 ? (
{ - setTrackElement("Cycle empty state"); - setCreateModal(true); - }, + type={EmptyStateType.PROJECT_CYCLES} + primaryButtonOnClick={() => { + setTrackElement("Cycle empty state"); + setCreateModal(true); }} - size="lg" - disabled={!isEditingAllowed} />
) : ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 45204541bcb..d299c218249 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -2,18 +2,9 @@ import { useState, Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; import { Tab } from "@headlessui/react"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesHeader } from "components/headers"; -import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { PagesLoader } from "components/ui"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PAGE_TABS_LIST } from "constants/page"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; @@ -22,9 +13,16 @@ import useSize from "hooks/use-window-size"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { EmptyState } from "components/empty-state"; +import { PagesHeader } from "components/headers"; +import { PagesLoader } from "components/ui"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { PAGE_TABS_LIST } from "constants/page"; +import { EmptyStateType } from "constants/empty-state"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId } = router.query; // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - currentUser, - currentUserLoader, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser, currentUserLoader } = useUser(); const { commandPalette: { toggleCreatePageModal }, } = useApplication(); @@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { }; // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; @@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { ) : ( { - setTrackElement("Pages empty state"); - toggleCreatePageModal(true); - }, - }} - comicBox={{ - title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title, - description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description, + type={EmptyStateType.PROJECT_PAGE} + primaryButtonOnClick={() => { + setTrackElement("Pages empty state"); + toggleCreatePageModal(true); }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index b227becf955..60e9ca61acd 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,17 +1,9 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { ProjectSettingHeader } from "components/headers"; -import { IntegrationCard } from "components/project"; import { IntegrationsSettingsLoader } from "components/ui"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; @@ -20,10 +12,17 @@ import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components +import { PageHead } from "components/core"; +import { IntegrationCard } from "components/project"; +import { ProjectSettingHeader } from "components/headers"; +import { EmptyState } from "components/empty-state"; // ui // types import { IProject } from "@plane/types"; // fetch-keys +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +// constants +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -32,10 +31,6 @@ const projectService = new ProjectService(); const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); // fetch project details const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -47,9 +42,6 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) ); // derived values - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); const isAdmin = projectDetails?.member_role === 20; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; @@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { ) : (
router.push(`/${workspaceSlug}/settings/integrations`), - }} - size="lg" - disabled={!isAdmin} + type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS} + primaryButtonLink={`/${workspaceSlug}/settings/integrations`} />
) diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 75d46b63df5..59c205968c8 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,28 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // store hooks -import { Button } from "@plane/ui"; -import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { APITokenSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { API_TOKENS_LIST } from "constants/fetch-keys"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component +import { APITokenSettingsLoader } from "components/ui"; +import { WorkspaceSettingHeader } from "components/headers"; +import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; +import { EmptyState } from "components/empty-state"; +import { PageHead } from "components/core"; // ui +import { Button } from "@plane/ui"; // services import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types // constants +import { API_TOKENS_LIST } from "constants/fetch-keys"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; const apiTokenService = new APITokenService(); @@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { currentWorkspace } = useWorkspace(); @@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; if (!isAdmin) @@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index d5058e29f2b..24dca325cb1 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,24 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { Button } from "@plane/ui"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhookSettingsLoader } from "components/ui"; -import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; +import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states @@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // mobx store const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); @@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; // clear secret key when modal is closed. @@ -99,12 +91,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/public/empty-state/module-issues/gantt-dark-resp.webp b/web/public/empty-state/module-issues/gantt_chart-dark-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-dark-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-dark.webp b/web/public/empty-state/module-issues/gantt_chart-dark.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark.webp rename to web/public/empty-state/module-issues/gantt_chart-dark.webp diff --git a/web/public/empty-state/module-issues/gantt-light-resp.webp b/web/public/empty-state/module-issues/gantt_chart-light-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-light-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-light.webp b/web/public/empty-state/module-issues/gantt_chart-light.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light.webp rename to web/public/empty-state/module-issues/gantt_chart-light.webp From da735f318a75a058fa1ea0cef8c2261d1fa5e949 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:17:32 +0530 Subject: [PATCH 029/214] [WEB-404] chore: calendar layout add existing issue workflow improvement (#3877) * chore: target date none filter * chore: calendar layout add existing issue functionality added for cycle and module * fix: enums export in the types package * chore: remove NestedKeyOf type --------- Co-authored-by: NarayanBavisetti Co-authored-by: Aaryan Khandelwal --- apiserver/plane/app/views/search.py | 4 + packages/types/src/projects.d.ts | 1 + .../calendar/base-calendar-root.tsx | 12 +- .../issue-layouts/calendar/calendar.tsx | 4 + .../issue-layouts/calendar/day-tile.tsx | 6 +- .../calendar/quick-add-issue-form.tsx | 108 +++++++++++++++--- .../calendar/roots/cycle-root.tsx | 18 ++- .../calendar/roots/module-root.tsx | 22 +++- .../issue-layouts/calendar/week-days.tsx | 3 + web/constants/dashboard.ts | 1 - 10 files changed, 147 insertions(+), 32 deletions(-) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index a2ed1c015ad..ba8e2e0c385 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -235,6 +235,7 @@ def get(self, request, slug, project_id): cycle = request.query_params.get("cycle", "false") module = request.query_params.get("module", False) sub_issue = request.query_params.get("sub_issue", "false") + target_date = request.query_params.get("target_date", True) issue_id = request.query_params.get("issue_id", False) @@ -273,6 +274,9 @@ def get(self, request, slug, project_id): if module: issues = issues.exclude(issue_module__module=module) + if target_date == "none": + issues = issues.filter(target_date__isnull=True) + return Response( issues.values( "name", diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a937341866c..a6da364b9b7 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = { sub_issue?: boolean; issue_id?: string; workspace_search: boolean; + target_date?: string; }; export interface ISearchIssueResponse { diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 2a8cbcc26e0..ab47a7399df 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -29,12 +29,21 @@ interface IBaseCalendarRoot { [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, isCompletedCycle = false } = props; + const { + issueStore, + issuesFilterStore, + QuickActions, + issueActions, + addIssuesToView, + viewId, + isCompletedCycle = false, + } = props; // router const router = useRouter(); @@ -128,6 +137,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { readOnly={!isEditingAllowed || isCompletedCycle} /> )} + addIssuesToView={addIssuesToView} quickAddCallback={issueStore.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 3089a45c474..30839326736 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -30,6 +30,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -43,6 +44,7 @@ export const CalendarChart: React.FC = observer((props) => { showWeekends, quickActions, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -90,6 +92,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> @@ -106,6 +109,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 849b967cec3..8ac1e460cb3 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite"; // components import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; // helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; @@ -27,6 +28,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -112,6 +115,7 @@ export const CalendarDayTile: React.FC = observer((props) => { target_date: renderFormattedPayloadDate(date.date) ?? undefined, }} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} onOpen={() => setShowAllIssues(true)} /> diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 5738e028e7c..5f62706dc4d 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -2,20 +2,24 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; +// components +import { ExistingIssuesListModal } from "components/core"; // hooks -import { PlusIcon } from "lucide-react"; -import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; -import { ISSUE_CREATED } from "constants/event-tracker"; -import { createIssuePayload } from "helpers/issue.helper"; -import { useEventTracker, useProject } from "hooks/store"; +import { useEventTracker, useIssueDetail, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers +import { createIssuePayload } from "helpers/issue.helper"; // icons +import { PlusIcon } from "lucide-react"; // ui +import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; +import { ISearchIssueResponse, TIssue } from "@plane/types"; // constants +import { ISSUE_CREATED } from "constants/event-tracker"; +// helper +import { cn } from "helpers/common.helper"; type Props = { formKey: keyof TIssue; @@ -28,6 +32,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; onOpen?: () => void; }; @@ -60,21 +65,26 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props; + const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); + const { updateIssue } = useIssueDetail(); // refs const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; + const ExistingIssuesListModalPayload = moduleId + ? { module: moduleId.toString(), target_date: "none" } + : { cycle: true, target_date: "none" }; const { reset, @@ -158,13 +168,50 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { } }; - const handleOpen = () => { + const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId) return; + + const issueIds = data.map((i) => i.id); + + try { + // To handle all updates in parallel + await Promise.all( + data.map((issue) => + updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) + ) + ); + if (addIssuesToView) { + await addIssuesToView(issueIds); + } + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + } + }; + + const handleNewIssue = () => { setIsOpen(true); if (onOpen) onOpen(); }; + const handleExistingIssue = () => { + setIsExistingIssueModalOpen(true); + }; return ( <> + {workspaceSlug && projectId && ( + setIsExistingIssueModalOpen(false)} + searchParams={ExistingIssuesListModalPayload} + handleOnSubmit={handleAddIssuesToView} + /> + )} {isOpen && (
= observer((props) => { )} {!isOpen && ( -
- +
+ {addIssuesToView ? ( + setIsMenuOpen(true)} + onMenuClose={() => setIsMenuOpen(false)} + className="w-full" + customButtonClassName="w-full" + customButton={ +
+ + New Issue +
+ } + > + New Issue + Add existing issue +
+ ) : ( + + )}
)} diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 7a30d187e92..80a21838d18 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,15 +1,16 @@ -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; //hooks -import { CycleIssueQuickActions } from "components/issues"; -import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components +import { CycleIssueQuickActions } from "components/issues"; +import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; -import { BaseCalendarRoot } from "../base-calendar-root"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const CycleCalendarLayout: React.FC = observer(() => { const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); @@ -46,11 +47,20 @@ export const CycleCalendarLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); + return ( { const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { + const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; @@ -42,12 +43,21 @@ export const ModuleCalendarLayout: React.FC = observer(() => { [issues, workspaceSlug, moduleId] ); + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }, + [issues?.addIssuesToModule, workspaceSlug, projectId, moduleId] + ); + return ( ); diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 2ce742fe85f..ec1d12e5937 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -25,6 +25,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -68,6 +70,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3d11b4f123c..3d99a467958 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -11,7 +11,6 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; - // constants import { EUserWorkspaceRoles } from "./workspace"; // icons From 3f1ce9907dc7c16b89b634c62a1db6239437ad8a Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 6 Mar 2024 20:18:47 +0530 Subject: [PATCH 030/214] fix: auto merge fixes --- .github/workflows/auto-merge.yml | 37 +++++++++++--------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 60ebe583418..ed381453219 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -8,13 +8,13 @@ on: env: CURRENT_BRANCH: ${{ github.ref_name }} - SOURCE_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The sync branch such as "sync/ce" - TARGET_BRANCH: ${{ secrets.TARGET_BRANCH }} # The target branch that you would like to merge changes like develop + SOURCE_BRANCH: ${{ secrets.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce" + TARGET_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows - REVIEWER: ${{ secrets.REVIEWER }} + REVIEWER: ${{ secrets.SYNC_PR_REVIEWER }} jobs: - Check_Branch: + Check_Branch: runs-on: ubuntu-latest outputs: BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }} @@ -27,7 +27,7 @@ jobs: else echo "MATCH=false" >> $GITHUB_OUTPUT fi - + Auto_Merge: if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }} needs: [Check_Branch] @@ -41,6 +41,11 @@ jobs: with: fetch-depth: 0 # Fetch all history for all branches and tags + - name: Setup Git + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + - name: Setup GH CLI and Git Config run: | type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) @@ -50,20 +55,6 @@ jobs: sudo apt update sudo apt install gh -y - - id: git-author - name: Setup Git CLI from Github Token - run: | - VIEWER_JSON=$(gh api graphql -f query='query { viewer { name login databaseId }}' --jq '.data.viewer') - VIEWER_NAME=$(jq --raw-output '.name | values' <<< "${VIEWER_JSON}") - VIEWER_LOGIN=$(jq --raw-output '.login' <<< "${VIEWER_JSON}") - VIEWER_DATABASE_ID=$(jq --raw-output '.databaseId' <<< "${VIEWER_JSON}") - - USER_NAME="${VIEWER_NAME:-${VIEWER_LOGIN}}" - USER_EMAIL="${VIEWER_DATABASE_ID}+${VIEWER_LOGIN}@users.noreply.github.com" - - git config --global user.name ${USER_NAME} - git config --global user.email ${USER_EMAIL} - - name: Check for merge conflicts id: conflicts run: | @@ -88,10 +79,6 @@ jobs: - name: Create PR to Target Branch if: env.HAS_CONFLICTS == 'true' run: | - # Use GitHub CLI to create PR and specify author and committer - PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH \ - --title "sync: merge conflicts need to be resolved" \ - --body "" \ - --reviewer $REVIEWER ) + # Replace 'username' with the actual GitHub username of the reviewer. + PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "" --reviewer $REVIEWER) echo "Pull Request created: $PR_URL" - From cb5198c883e2478252ac2f94f288519945fee691 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:38:21 +0530 Subject: [PATCH 031/214] fix: breadcrumb loading state for the issue details page (#3892) Co-authored-by: sriram veeraghanta --- .../project-archived-issue-details.tsx | 10 +++---- .../headers/project-issue-details.tsx | 28 +++++++------------ web/components/project/integration-card.tsx | 1 - 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 86dae643d30..7cf5c567385 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -13,7 +13,6 @@ import { ProjectLogo } from "components/project"; // ui // types import { IssueArchiveService } from "services/issue"; -import { TIssue } from "@plane/types"; // constants // services // helpers @@ -26,9 +25,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, archivedIssueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); - const { data: issueDetails } = useSWR( + const { data: issueDetails } = useSWR( workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null, workspaceSlug && projectId && archivedIssueId ? () => @@ -79,8 +78,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { link={ } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index b9343a15cac..080a345601e 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -1,41 +1,32 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import useSWR from "swr"; // hooks import { PanelRight } from "lucide-react"; import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { ISSUE_DETAILS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; -import { useApplication, useProject } from "hooks/store"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui // helpers // services -import { IssueService } from "services/issue"; import { ProjectLogo } from "components/project"; // constants // components -// services -const issueService = new IssueService(); - export const ProjectIssueDetailsHeader: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); const { theme: themeStore } = useApplication(); - - const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed; return ( @@ -77,8 +68,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { link={ } diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index 39423849747..17f490bc39e 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -13,7 +13,6 @@ import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; // types import { IWorkspaceIntegration } from "@plane/types"; -// services import { ProjectService } from "services/project"; type Props = { From 549f6d0943333dbc80cce51fa919e772eb19eb11 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:38:57 +0530 Subject: [PATCH 032/214] [WEB-438] fix: ai insertion behaviour (#3872) * fixed ai insertion behaviour * replaced all ai popover references to have similar behavior * chore: removed debug statements --- packages/editor/core/package.json | 2 +- .../insert-content-at-cursor-position.ts | 17 +++++++++++++++++ packages/editor/core/src/hooks/use-editor.tsx | 15 +++++++++++++-- .../editor/document-editor/src/ui/index.tsx | 1 + .../editor/rich-text-editor/src/ui/index.tsx | 1 + .../inbox/modals/create-issue-modal.tsx | 4 +--- web/components/issues/issue-modal/form.tsx | 3 +-- .../projects/[projectId]/pages/[pageId].tsx | 9 +++------ web/store/page.store.ts | 6 +++--- yarn.lock | 8 ++++---- 10 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 packages/editor/core/src/helpers/insert-content-at-cursor-position.ts diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 198b21b0f68..571fb858864 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -53,7 +53,7 @@ "react-moveable": "^0.54.2", "tailwind-merge": "^1.14.0", "tippy.js": "^6.3.7", - "tiptap-markdown": "^0.8.2" + "tiptap-markdown": "^0.8.9" }, "devDependencies": { "@types/node": "18.15.3", diff --git a/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts new file mode 100644 index 00000000000..062acafcb1b --- /dev/null +++ b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts @@ -0,0 +1,17 @@ +import { Selection } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/react"; +import { MutableRefObject } from "react"; + +export const insertContentAtSavedSelection = ( + editorRef: MutableRefObject, + content: string, + savedSelection: Selection +) => { + if (editorRef.current && savedSelection) { + editorRef.current + .chain() + .focus() + .insertContentAt(savedSelection?.anchor, content) + .run(); + } +}; diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index c2923c1e97d..7e6aa5912a0 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -1,5 +1,5 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; -import { useImperativeHandle, useRef, MutableRefObject } from "react"; +import { useImperativeHandle, useRef, MutableRefObject, useState } from "react"; import { CoreEditorProps } from "src/ui/props"; import { CoreEditorExtensions } from "src/ui/extensions"; import { EditorProps } from "@tiptap/pm/view"; @@ -8,6 +8,8 @@ import { DeleteImage } from "src/types/delete-image"; import { IMentionSuggestion } from "src/types/mention-suggestion"; import { RestoreImage } from "src/types/restore-image"; import { UploadImage } from "src/types/upload-image"; +import { Selection } from "@tiptap/pm/state"; +import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cursor-position"; interface CustomEditorProps { uploadFile: UploadImage; @@ -70,8 +72,10 @@ export const useEditor = ({ onCreate: async ({ editor }) => { onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); }, + onTransaction: async ({ editor }) => { + setSavedSelection(editor.state.selection); + }, onUpdate: async ({ editor }) => { - // for instant feedback loop setIsSubmitting?.("submitting"); setShouldShowAlert?.(true); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); @@ -83,6 +87,8 @@ export const useEditor = ({ const editorRef: MutableRefObject = useRef(null); editorRef.current = editor; + const [savedSelection, setSavedSelection] = useState(null); + useImperativeHandle(forwardedRef, () => ({ clearEditor: () => { editorRef.current?.commands.clearContent(); @@ -90,6 +96,11 @@ export const useEditor = ({ setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content); }, + setEditorValueAtCursorPosition: (content: string) => { + if (savedSelection) { + insertContentAtSavedSelection(editorRef, content, savedSelection); + } + }, })); if (!editor) { diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index 2491e04c7f4..e9f6d884b21 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -55,6 +55,7 @@ interface DocumentEditorProps extends IDocumentEditor { interface EditorHandle { clearEditor: () => void; setEditorValue: (content: string) => void; + setEditorValueAtCursorPosition: (content: string) => void; } const DocumentEditor = ({ diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 4bcb340fd80..2aff5d26561 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -45,6 +45,7 @@ export interface RichTextEditorProps extends IRichTextEditor { interface EditorHandle { clearEditor: () => void; setEditorValue: (content: string) => void; + setEditorValueAtCursorPosition: (content: string) => void; } const RichTextEditor = ({ diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 5a3e614a9b7..2603b712ef6 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -119,9 +119,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - // setValue("description", {}); - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const handleAutoGenerateDescription = async () => { diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 527ebd0e183..03a9ae5b052 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -180,8 +180,7 @@ export const IssueFormRoot: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const handleAutoGenerateDescription = async () => { diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 3a133ee5098..16dba79b39c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -53,7 +53,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { membership: { currentProjectRole }, } = useUser(); - const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ + const { handleSubmit, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, }); @@ -124,16 +124,13 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const updatePage = async (formData: IPage) => { if (!workspaceSlug || !projectId || !pageId) return; - await updateDescriptionAction(formData.description_html); + updateDescriptionAction(formData.description_html); }; const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId || !pageId) return; - const newDescription = `${watch("description_html")}

${response}

`; - setValue("description_html", newDescription); - editorRef.current?.setEditorValue(newDescription); - updateDescriptionAction(newDescription); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const actionCompleteAlert = ({ diff --git a/web/store/page.store.ts b/web/store/page.store.ts index ae416237f37..30fc3d157f7 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -35,7 +35,7 @@ export interface IPageStore { addToFavorites: () => Promise; removeFromFavorites: () => Promise; updateName: (name: string) => Promise; - updateDescription: (description: string) => Promise; + updateDescription: (description: string) => void; // Reactions disposers: Array<() => void>; @@ -89,7 +89,7 @@ export class PageStore implements IPageStore { addToFavorites: action, removeFromFavorites: action, updateName: action, - updateDescription: action, + updateDescription: action.bound, setIsSubmitting: action, cleanup: action, }); @@ -166,7 +166,7 @@ export class PageStore implements IPageStore { this.name = name; }); - updateDescription = action("updateDescription", async (description_html: string) => { + updateDescription = action("updateDescription", (description_html: string) => { const { projectId, workspaceSlug } = this.rootStore.app.router; if (!projectId || !workspaceSlug) return; diff --git a/yarn.lock b/yarn.lock index c8cfcffd4af..66fef83d16d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8153,10 +8153,10 @@ tippy.js@^6.3.1, tippy.js@^6.3.7: dependencies: "@popperjs/core" "^2.9.0" -tiptap-markdown@^0.8.2: - version "0.8.8" - resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.8.tgz#1e25f40b726239dff84b99a53eb1bdf4af0a02f9" - integrity sha512-I2w/IpvCZ1BoR3nQzG0wRK3uGmDv+Ohyr++G24Ma6RzoDYd0TVGXZp0BOODX5Jj4c6heVY8eksahSeAwJMZBeg== +tiptap-markdown@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.9.tgz#e13f3ae9a1b1649f8c28bb3cae4516a53da7492c" + integrity sha512-TykSDcsb94VFCzPbSSTfB6Kh2HJi7x4B9J3Jm9uSOAMPy8App1YfrLW/rEJLajTxwMVhWBdOo4nidComSlLQsQ== dependencies: "@types/markdown-it" "^12.2.3" markdown-it "^13.0.1" From ed8782757d2ee70779e36d18d2cfecbbd3cd4b69 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:39:50 +0530 Subject: [PATCH 033/214] [WEB - 471] dev: caching users and workspace apis (#3707) * dev: caching users and workspace apis * dev: cache user and config apis * dev: update caching function to use user_id instead of token * dev: update caching layer * dev: update caching logic * dev: format caching file * dev: refactor caching to include name space and user id as key * dev: cache project cover image endpoint --- apiserver/plane/app/views/config.py | 4 +- apiserver/plane/app/views/estimate.py | 5 +- apiserver/plane/app/views/issue.py | 39 +++++---- apiserver/plane/app/views/project.py | 4 +- apiserver/plane/app/views/state.py | 8 +- apiserver/plane/app/views/user.py | 21 ++++- apiserver/plane/app/views/workspace.py | 41 +++++++-- apiserver/plane/license/api/views/instance.py | 20 +++-- apiserver/plane/settings/local.py | 7 +- apiserver/plane/utils/cache.py | 84 +++++++++++++++++++ 10 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 apiserver/plane/utils/cache.py diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index b2a27252cba..354f0aebc5b 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -12,13 +12,14 @@ # Module imports from .base import BaseAPIView from plane.license.utils.instance_value import get_configuration_value - +from plane.utils.cache import cache_response class ConfigurationEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): # Get all the configuration ( @@ -136,6 +137,7 @@ class MobileConfigurationEndpoint(BaseAPIView): AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): ( GOOGLE_CLIENT_ID, diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate.py index 3402bb06864..eae2e3351dc 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -11,7 +11,7 @@ EstimatePointSerializer, EstimateReadSerializer, ) - +from plane.utils.cache import invalidate_cache class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ @@ -49,6 +49,7 @@ def list(self, request, slug, project_id): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def create(self, request, slug, project_id): if not request.data.get("estimate", False): return Response( @@ -114,6 +115,7 @@ def retrieve(self, request, slug, project_id, estimate_id): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def partial_update(self, request, slug, project_id, estimate_id): if not request.data.get("estimate", False): return Response( @@ -182,6 +184,7 @@ def partial_update(self, request, slug, project_id, estimate_id): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 14e0b6a9aa9..4355f0ab573 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -78,6 +78,7 @@ from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from collections import defaultdict +from plane.utils.cache import invalidate_cache class IssueListEndpoint(BaseAPIView): @@ -1001,6 +1002,21 @@ class LabelViewSet(BaseViewSet): ProjectMemberPermission, ] + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("parent") + .distinct() + .order_by("sort_order") + ) + + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -1020,22 +1036,13 @@ def create(self, request, slug, project_id): status=status.HTTP_400_BAD_REQUEST, ) - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("parent") - .distinct() - .order_by("sort_order") - ) + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) class BulkDeleteIssuesEndpoint(BaseAPIView): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 6f9b2618e19..42b9c1f3784 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -65,7 +65,7 @@ ) from plane.bgtasks.project_invitation_task import project_invitation - +from plane.utils.cache import cache_response class ProjectViewSet(WebhookMixin, BaseViewSet): serializer_class = ProjectListSerializer @@ -1045,6 +1045,8 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): AllowAny, ] + # Cache the below api for 24 hours + @cache_response(60 * 60 * 24, user=False) def get(self, request): files = [] s3 = boto3.client( diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state.py index 34b3d1dcc01..6d4fd778274 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -9,14 +9,13 @@ from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView +from . import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( ProjectEntityPermission, - WorkspaceEntityPermission, ) from plane.db.models import State, Issue - +from plane.utils.cache import invalidate_cache class StateViewSet(BaseViewSet): serializer_class = StateSerializer @@ -41,6 +40,7 @@ def get_queryset(self): .distinct() ) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def create(self, request, slug, project_id): serializer = StateSerializer(data=request.data) if serializer.is_valid(): @@ -61,6 +61,7 @@ def list(self, request, slug, project_id): return Response(state_dict, status=status.HTTP_200_OK) return Response(states, status=status.HTTP_200_OK) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def mark_as_default(self, request, slug, project_id, pk): # Select all the states which are marked as default _ = State.objects.filter( @@ -71,6 +72,7 @@ def mark_as_default(self, request, slug, project_id, pk): ).update(default=True) return Response(status=status.HTTP_204_NO_CONTENT) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def destroy(self, request, slug, project_id, pk): state = State.objects.get( ~Q(name="Triage"), diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user.py index 7764e3b9753..07049b8d571 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -1,8 +1,10 @@ +# Django imports +from django.db.models import Q, Count, Case, When, IntegerField + # Third party imports from rest_framework.response import Response from rest_framework import status - # Module imports from plane.app.serializers import ( UserSerializer, @@ -15,9 +17,7 @@ from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember from plane.license.models import Instance, InstanceAdmin from plane.utils.paginator import BasePaginator - - -from django.db.models import Q, F, Count, Case, When, IntegerField +from plane.utils.cache import cache_response, invalidate_cache class UserEndpoint(BaseViewSet): @@ -27,6 +27,7 @@ class UserEndpoint(BaseViewSet): def get_object(self): return self.request.user + @cache_response(60 * 60) def retrieve(self, request): serialized_data = UserMeSerializer(request.user).data return Response( @@ -34,10 +35,12 @@ def retrieve(self, request): status=status.HTTP_200_OK, ) + @cache_response(60 * 60) def retrieve_user_settings(self, request): serialized_data = UserMeSettingsSerializer(request.user).data return Response(serialized_data, status=status.HTTP_200_OK) + @cache_response(60 * 60) def retrieve_instance_admin(self, request): instance = Instance.objects.first() is_admin = InstanceAdmin.objects.filter( @@ -47,6 +50,11 @@ def retrieve_instance_admin(self, request): {"is_instance_admin": is_admin}, status=status.HTTP_200_OK ) + @invalidate_cache(path="/api/users/me/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/users/me/") def deactivate(self, request): # Check all workspace user is active user = self.get_object() @@ -145,6 +153,8 @@ def deactivate(self, request): class UpdateUserOnBoardedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_onboarded = request.data.get("is_onboarded", False) @@ -155,6 +165,8 @@ def patch(self, request): class UpdateUserTourCompletedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_tour_completed = request.data.get("is_tour_completed", False) @@ -165,6 +177,7 @@ def patch(self, request): class UserActivityEndpoint(BaseAPIView, BasePaginator): + def get(self, request): queryset = IssueActivity.objects.filter( actor=request.user diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 7c4a5db8d75..34765c3c728 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -57,6 +57,8 @@ WorkspaceEstimateSerializer, StateSerializer, LabelSerializer, + CycleSerializer, + ModuleSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -77,7 +79,6 @@ Label, WorkspaceMember, CycleIssue, - IssueReaction, WorkspaceUserProperties, Estimate, EstimatePoint, @@ -91,17 +92,11 @@ WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission, - ProjectLitePermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.app.serializers.module import ( - ModuleSerializer, -) -from plane.app.serializers.cycle import ( - CycleSerializer, -) +from plane.utils.cache import cache_response, invalidate_cache class WorkSpaceViewSet(BaseViewSet): @@ -151,7 +146,8 @@ def get_queryset(self): .annotate(total_issues=issue_count) .select_related("owner") ) - + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") def create(self, request): try: serializer = WorkSpaceSerializer(data=request.data) @@ -197,6 +193,20 @@ def create(self, request): status=status.HTTP_410_GONE, ) + @cache_response(60 * 60 * 2) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + class UserWorkSpacesEndpoint(BaseAPIView): search_fields = [ @@ -206,6 +216,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): "owner", ] + @cache_response(60 * 60 * 2) def get(self, request): fields = [ field @@ -403,6 +414,8 @@ class WorkspaceJoinEndpoint(BaseAPIView): ] """Invitation response endpoint the user can respond to the invitation""" + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") def post(self, request, slug, pk): workspace_invite = WorkspaceMemberInvite.objects.get( pk=pk, workspace__slug=slug @@ -499,6 +512,9 @@ def get_queryset(self): .annotate(total_members=Count("workspace__workspace_member")) ) + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def create(self, request): invitations = request.data.get("invitations", []) workspace_invitations = WorkspaceMemberInvite.objects.filter( @@ -569,6 +585,7 @@ def get_queryset(self): .select_related("member") ) + @cache_response(60 * 60 * 2) def list(self, request, slug): workspace_member = WorkspaceMember.objects.get( member=request.user, @@ -593,6 +610,7 @@ def list(self, request, slug): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def partial_update(self, request, slug, pk): workspace_member = WorkspaceMember.objects.get( pk=pk, @@ -635,6 +653,7 @@ def partial_update(self, request, slug, pk): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def destroy(self, request, slug, pk): # Check the user role who is deleting the user workspace_member = WorkspaceMember.objects.get( @@ -699,6 +718,7 @@ def destroy(self, request, slug, pk): workspace_member.save() return Response(status=status.HTTP_204_NO_CONTENT) + @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) def leave(self, request, slug): workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, @@ -1550,6 +1570,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView): WorkspaceViewerPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): labels = Label.objects.filter( workspace__slug=slug, @@ -1565,6 +1586,7 @@ class WorkspaceStatesEndpoint(BaseAPIView): WorkspaceEntityPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): states = State.objects.filter( workspace__slug=slug, @@ -1580,6 +1602,7 @@ class WorkspaceEstimatesEndpoint(BaseAPIView): WorkspaceEntityPermission, ] + @cache_response(60 * 60 * 2) def get(self, request, slug): estimate_ids = Project.objects.filter( workspace__slug=slug, estimate__isnull=False diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 112c68bc89a..c8608cbe579 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -1,17 +1,11 @@ # Python imports -import json -import os -import requests import uuid -import random -import string # Django imports from django.utils import timezone from django.contrib.auth.hashers import make_password from django.core.validators import validate_email from django.core.exceptions import ValidationError -from django.conf import settings # Third party imports from rest_framework import status @@ -30,9 +24,9 @@ from plane.license.api.permissions import ( InstanceAdminPermission, ) -from plane.db.models import User, WorkspaceMember, ProjectMember +from plane.db.models import User from plane.license.utils.encryption import encrypt_data - +from plane.utils.cache import cache_response, invalidate_cache class InstanceEndpoint(BaseAPIView): def get_permissions(self): @@ -44,6 +38,7 @@ def get_permissions(self): AllowAny(), ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance = Instance.objects.first() # get the instance @@ -58,6 +53,7 @@ def get(self, request): data["is_activated"] = True return Response(data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def patch(self, request): # Get the instance instance = Instance.objects.first() @@ -75,6 +71,7 @@ class InstanceAdminEndpoint(BaseAPIView): InstanceAdminPermission, ] + @invalidate_cache(path="/api/instances/", user=False) # Create an instance admin def post(self, request): email = request.data.get("email", False) @@ -104,6 +101,7 @@ def post(self, request): serializer = InstanceAdminSerializer(instance_admin) return Response(serializer.data, status=status.HTTP_201_CREATED) + @cache_response(60 * 60 * 2) def get(self, request): instance = Instance.objects.first() if instance is None: @@ -115,6 +113,7 @@ def get(self, request): serializer = InstanceAdminSerializer(instance_admins, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def delete(self, request, pk): instance = Instance.objects.first() instance_admin = InstanceAdmin.objects.filter( @@ -128,6 +127,7 @@ class InstanceConfigurationEndpoint(BaseAPIView): InstanceAdminPermission, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance_configurations = InstanceConfiguration.objects.all() serializer = InstanceConfigurationSerializer( @@ -135,6 +135,8 @@ def get(self, request): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/configs/", user=False) + @invalidate_cache(path="/api/mobile-configs/", user=False) def patch(self, request): configurations = InstanceConfiguration.objects.filter( key__in=request.data.keys() @@ -170,6 +172,7 @@ class InstanceAdminSignInEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): # Check instance first instance = Instance.objects.first() @@ -260,6 +263,7 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): instance = Instance.objects.first() if instance is None: diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 8f27d423418..4dc998e55b4 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -1,4 +1,5 @@ """Development settings""" + from .common import * # noqa DEBUG = True @@ -14,7 +15,11 @@ CACHES = { "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, } } diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py new file mode 100644 index 00000000000..dba89c4a62d --- /dev/null +++ b/apiserver/plane/utils/cache.py @@ -0,0 +1,84 @@ +from django.core.cache import cache +# from django.utils.encoding import force_bytes +# import hashlib +from functools import wraps +from rest_framework.response import Response + + +def generate_cache_key(custom_path, auth_header=None): + """Generate a cache key with the given params""" + if auth_header: + key_data = f"{custom_path}:{auth_header}" + else: + key_data = custom_path + return key_data + + +def cache_response(timeout=60 * 60, path=None, user=True): + """decorator to create cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Function to generate cache key + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, auth_header) + cached_result = cache.get(key) + if cached_result is not None: + print("Cache Hit") + return Response( + cached_result["data"], status=cached_result["status"] + ) + + print("Cache Miss") + response = view_func(instance, request, *args, **kwargs) + + if response.status_code == 200: + cache.set( + key, + {"data": response.data, "status": response.status_code}, + timeout, + ) + + return response + + return _wrapped_view + + return decorator + + +def invalidate_cache(path=None, url_params=False, user=True): + """invalidate cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Invalidate cache before executing the view function + if url_params: + path_with_values = path + for key, value in kwargs.items(): + path_with_values = path_with_values.replace( + f":{key}", str(value) + ) + + custom_path = path_with_values + else: + custom_path = ( + path if path is not None else request.get_full_path() + ) + + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + key = generate_cache_key(custom_path, auth_header) + cache.delete(key) + print("Invalidating cache") + # Execute the view function + return view_func(instance, request, *args, **kwargs) + + return _wrapped_view + + return decorator From a852e3cc523bf368635d4c91cb76ecdeb98defaf Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:41:45 +0530 Subject: [PATCH 034/214] chore: integrations and importers (#3630) * dev: update imports to use jira oauth * dev: remove integration and importer folders and files --- apiserver/plane/app/serializers/__init__.py | 10 - .../app/serializers/integration/__init__.py | 8 - .../plane/app/serializers/integration/base.py | 22 - .../app/serializers/integration/github.py | 45 -- .../app/serializers/integration/slack.py | 14 - apiserver/plane/app/urls/__init__.py | 4 - apiserver/plane/app/urls/external.py | 6 - apiserver/plane/app/urls/importer.py | 37 -- apiserver/plane/app/urls/integration.py | 150 ----- apiserver/plane/app/urls/issue.py | 35 +- apiserver/plane/app/urls/module.py | 6 - apiserver/plane/app/views/__init__.py | 20 - apiserver/plane/app/views/external.py | 7 - apiserver/plane/app/views/importer.py | 558 ------------------ .../plane/app/views/integration/__init__.py | 9 - apiserver/plane/app/views/integration/base.py | 181 ------ .../plane/app/views/integration/github.py | 202 ------- .../plane/app/views/integration/slack.py | 96 --- apiserver/plane/bgtasks/importer_task.py | 201 ------- .../plane/db/models/social_connection.py | 2 +- apiserver/plane/utils/importers/__init__.py | 0 apiserver/plane/utils/importers/jira.py | 117 ---- .../plane/utils/integrations/__init__.py | 0 apiserver/plane/utils/integrations/github.py | 154 ----- apiserver/plane/utils/integrations/slack.py | 21 - 25 files changed, 14 insertions(+), 1891 deletions(-) delete mode 100644 apiserver/plane/app/serializers/integration/__init__.py delete mode 100644 apiserver/plane/app/serializers/integration/base.py delete mode 100644 apiserver/plane/app/serializers/integration/github.py delete mode 100644 apiserver/plane/app/serializers/integration/slack.py delete mode 100644 apiserver/plane/app/urls/importer.py delete mode 100644 apiserver/plane/app/urls/integration.py delete mode 100644 apiserver/plane/app/views/importer.py delete mode 100644 apiserver/plane/app/views/integration/__init__.py delete mode 100644 apiserver/plane/app/views/integration/base.py delete mode 100644 apiserver/plane/app/views/integration/github.py delete mode 100644 apiserver/plane/app/views/integration/slack.py delete mode 100644 apiserver/plane/bgtasks/importer_task.py delete mode 100644 apiserver/plane/utils/importers/__init__.py delete mode 100644 apiserver/plane/utils/importers/jira.py delete mode 100644 apiserver/plane/utils/integrations/__init__.py delete mode 100644 apiserver/plane/utils/integrations/github.py delete mode 100644 apiserver/plane/utils/integrations/slack.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 9bdd4baaf9d..95651b800c0 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -86,16 +86,6 @@ from .api import APITokenSerializer, APITokenReadSerializer -from .integration import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, - GithubIssueSyncSerializer, - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, - SlackProjectSyncSerializer, -) - from .importer import ImporterSerializer from .page import ( diff --git a/apiserver/plane/app/serializers/integration/__init__.py b/apiserver/plane/app/serializers/integration/__init__.py deleted file mode 100644 index 112ff02d162..00000000000 --- a/apiserver/plane/app/serializers/integration/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .base import IntegrationSerializer, WorkspaceIntegrationSerializer -from .github import ( - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubIssueSyncSerializer, - GithubCommentSyncSerializer, -) -from .slack import SlackProjectSyncSerializer diff --git a/apiserver/plane/app/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py deleted file mode 100644 index 01e484ed027..00000000000 --- a/apiserver/plane/app/serializers/integration/base.py +++ /dev/null @@ -1,22 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import Integration, WorkspaceIntegration - - -class IntegrationSerializer(BaseSerializer): - class Meta: - model = Integration - fields = "__all__" - read_only_fields = [ - "verified", - ] - - -class WorkspaceIntegrationSerializer(BaseSerializer): - integration_detail = IntegrationSerializer( - read_only=True, source="integration" - ) - - class Meta: - model = WorkspaceIntegration - fields = "__all__" diff --git a/apiserver/plane/app/serializers/integration/github.py b/apiserver/plane/app/serializers/integration/github.py deleted file mode 100644 index 850bccf1b3a..00000000000 --- a/apiserver/plane/app/serializers/integration/github.py +++ /dev/null @@ -1,45 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import ( - GithubIssueSync, - GithubRepository, - GithubRepositorySync, - GithubCommentSync, -) - - -class GithubRepositorySerializer(BaseSerializer): - class Meta: - model = GithubRepository - fields = "__all__" - - -class GithubRepositorySyncSerializer(BaseSerializer): - repo_detail = GithubRepositorySerializer(source="repository") - - class Meta: - model = GithubRepositorySync - fields = "__all__" - - -class GithubIssueSyncSerializer(BaseSerializer): - class Meta: - model = GithubIssueSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - ] - - -class GithubCommentSyncSerializer(BaseSerializer): - class Meta: - model = GithubCommentSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - "issue_sync", - ] diff --git a/apiserver/plane/app/serializers/integration/slack.py b/apiserver/plane/app/serializers/integration/slack.py deleted file mode 100644 index 9c461c5b9b5..00000000000 --- a/apiserver/plane/app/serializers/integration/slack.py +++ /dev/null @@ -1,14 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import SlackProjectSync - - -class SlackProjectSyncSerializer(BaseSerializer): - class Meta: - model = SlackProjectSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "workspace_integration", - ] diff --git a/apiserver/plane/app/urls/__init__.py b/apiserver/plane/app/urls/__init__.py index f2b11f12761..40b96687d33 100644 --- a/apiserver/plane/app/urls/__init__.py +++ b/apiserver/plane/app/urls/__init__.py @@ -6,9 +6,7 @@ from .dashboard import urlpatterns as dashboard_urls from .estimate import urlpatterns as estimate_urls from .external import urlpatterns as external_urls -from .importer import urlpatterns as importer_urls from .inbox import urlpatterns as inbox_urls -from .integration import urlpatterns as integration_urls from .issue import urlpatterns as issue_urls from .module import urlpatterns as module_urls from .notification import urlpatterns as notification_urls @@ -32,9 +30,7 @@ *dashboard_urls, *estimate_urls, *external_urls, - *importer_urls, *inbox_urls, - *integration_urls, *issue_urls, *module_urls, *notification_urls, diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py index 774e6fb7cd3..8db87a24928 100644 --- a/apiserver/plane/app/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -2,7 +2,6 @@ from plane.app.views import UnsplashEndpoint -from plane.app.views import ReleaseNotesEndpoint from plane.app.views import GPTIntegrationEndpoint @@ -12,11 +11,6 @@ UnsplashEndpoint.as_view(), name="unsplash", ), - path( - "release-notes/", - ReleaseNotesEndpoint.as_view(), - name="release-notes", - ), path( "workspaces//projects//ai-assistant/", GPTIntegrationEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/importer.py b/apiserver/plane/app/urls/importer.py deleted file mode 100644 index f3a018d7894..00000000000 --- a/apiserver/plane/app/urls/importer.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, -) - - -urlpatterns = [ - path( - "workspaces//importers//", - ServiceIssueImportSummaryEndpoint.as_view(), - name="importer-summary", - ), - path( - "workspaces//projects/importers//", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers/", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers///", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects//service//importers//", - UpdateServiceImportStatusEndpoint.as_view(), - name="importer-status", - ), -] diff --git a/apiserver/plane/app/urls/integration.py b/apiserver/plane/app/urls/integration.py deleted file mode 100644 index cf3f82d5a49..00000000000 --- a/apiserver/plane/app/urls/integration.py +++ /dev/null @@ -1,150 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - IntegrationViewSet, - WorkspaceIntegrationViewSet, - GithubRepositoriesEndpoint, - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - GithubCommentSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) - - -urlpatterns = [ - path( - "integrations/", - IntegrationViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="integrations", - ), - path( - "integrations//", - IntegrationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="integrations", - ), - path( - "workspaces//workspace-integrations/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "list", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//", - WorkspaceIntegrationViewSet.as_view( - { - "post": "create", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//provider/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="workspace-integrations", - ), - # Github Integrations - path( - "workspaces//workspace-integrations//github-repositories/", - GithubRepositoriesEndpoint.as_view(), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync/", - GithubRepositorySyncViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync//", - GithubRepositorySyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync/", - GithubIssueSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", - BulkCreateGithubIssueSyncEndpoint.as_view(), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubIssueSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", - GithubCommentSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", - GithubCommentSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - ## End Github Integrations - # Slack Integration - path( - "workspaces//projects//workspace-integrations//project-slack-sync/", - SlackProjectSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//project-slack-sync//", - SlackProjectSyncViewSet.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - ), - ## End Slack Integration -] diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 4ee70450b37..6b677287b91 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -1,30 +1,27 @@ from django.urls import path - from plane.app.views import ( - IssueListEndpoint, - IssueViewSet, - LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, - BulkImportIssuesEndpoint, - UserWorkSpaceIssues, - SubIssuesEndpoint, - IssueLinkViewSet, - IssueAttachmentEndpoint, + CommentReactionViewSet, ExportIssuesEndpoint, IssueActivityEndpoint, + IssueArchiveViewSet, + IssueAttachmentEndpoint, IssueCommentViewSet, - IssueSubscriberViewSet, + IssueDraftViewSet, + IssueLinkViewSet, + IssueListEndpoint, IssueReactionViewSet, - CommentReactionViewSet, - IssueUserDisplayPropertyEndpoint, - IssueArchiveViewSet, IssueRelationViewSet, - IssueDraftViewSet, + IssueSubscriberViewSet, + IssueUserDisplayPropertyEndpoint, + IssueViewSet, + LabelViewSet, + SubIssuesEndpoint, + UserWorkSpaceIssues, ) - urlpatterns = [ path( "workspaces//projects//issues/list/", @@ -85,18 +82,12 @@ BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), - path( - "workspaces//projects//bulk-import-issues//", - BulkImportIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - # deprecated endpoint TODO: remove once confirmed path( "workspaces//my-issues/", UserWorkSpaceIssues.as_view(), name="workspace-issues", ), - ## + ## path( "workspaces//projects//issues//sub-issues/", SubIssuesEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 5e9f4f1230c..981b4d1fb5b 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -6,7 +6,6 @@ ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, - BulkImportModulesEndpoint, ModuleUserPropertiesEndpoint, ) @@ -106,11 +105,6 @@ ), name="user-favorite-module", ), - path( - "workspaces//projects//bulk-import-modules//", - BulkImportModulesEndpoint.as_view(), - name="bulk-modules-create", - ), path( "workspaces//projects//modules//user-properties/", ModuleUserPropertiesEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 910ea006d6d..7a311a78d69 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -117,25 +117,6 @@ from .api import ApiTokenEndpoint -from .integration import ( - WorkspaceIntegrationViewSet, - IntegrationViewSet, - GithubIssueSyncViewSet, - GithubRepositorySyncViewSet, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) - -from .importer import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, - BulkImportIssuesEndpoint, - BulkImportModulesEndpoint, -) - from .page import ( PageViewSet, PageFavoriteViewSet, @@ -148,7 +129,6 @@ from .external import ( GPTIntegrationEndpoint, - ReleaseNotesEndpoint, UnsplashEndpoint, ) diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external.py index 618c65e3ccb..f33e6290ff6 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external.py @@ -18,7 +18,6 @@ ProjectLiteSerializer, WorkspaceLiteSerializer, ) -from plane.utils.integrations.github import get_release_notes from plane.license.utils.instance_value import get_configuration_value @@ -85,12 +84,6 @@ def post(self, request, slug, project_id): ) -class ReleaseNotesEndpoint(BaseAPIView): - def get(self, request): - release_notes = get_release_notes() - return Response(release_notes, status=status.HTTP_200_OK) - - class UnsplashEndpoint(BaseAPIView): def get(self, request): (UNSPLASH_ACCESS_KEY,) = get_configuration_value( diff --git a/apiserver/plane/app/views/importer.py b/apiserver/plane/app/views/importer.py deleted file mode 100644 index a15ed36b761..00000000000 --- a/apiserver/plane/app/views/importer.py +++ /dev/null @@ -1,558 +0,0 @@ -# Python imports -import uuid - -# Third party imports -from rest_framework import status -from rest_framework.response import Response - -# Django imports -from django.db.models import Max, Q - -# Module imports -from plane.app.views import BaseAPIView -from plane.db.models import ( - WorkspaceIntegration, - Importer, - APIToken, - Project, - State, - IssueSequence, - Issue, - IssueActivity, - IssueComment, - IssueLink, - IssueLabel, - Workspace, - IssueAssignee, - Module, - ModuleLink, - ModuleIssue, - Label, -) -from plane.app.serializers import ( - ImporterSerializer, - IssueFlatSerializer, - ModuleSerializer, -) -from plane.utils.integrations.github import get_github_repo_details -from plane.utils.importers.jira import ( - jira_project_issue_summary, - is_allowed_hostname, -) -from plane.bgtasks.importer_task import service_importer -from plane.utils.html_processor import strip_tags -from plane.app.permissions import WorkSpaceAdminPermission - - -class ServiceIssueImportSummaryEndpoint(BaseAPIView): - def get(self, request, slug, service): - if service == "github": - owner = request.GET.get("owner", False) - repo = request.GET.get("repo", False) - - if not owner or not repo: - return Response( - {"error": "Owner and repo are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_integration = WorkspaceIntegration.objects.get( - integration__provider="github", workspace__slug=slug - ) - - access_tokens_url = workspace_integration.metadata.get( - "access_tokens_url", False - ) - - if not access_tokens_url: - return Response( - { - "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_count, labels, collaborators = get_github_repo_details( - access_tokens_url, owner, repo - ) - return Response( - { - "issue_count": issue_count, - "labels": labels, - "collaborators": collaborators, - }, - status=status.HTTP_200_OK, - ) - - if service == "jira": - # Check for all the keys - params = { - "project_key": "Project key is required", - "api_token": "API token is required", - "email": "Email is required", - "cloud_hostname": "Cloud hostname is required", - } - - for key, error_message in params.items(): - if not request.GET.get(key, False): - return Response( - {"error": error_message}, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_key = request.GET.get("project_key", "") - api_token = request.GET.get("api_token", "") - email = request.GET.get("email", "") - cloud_hostname = request.GET.get("cloud_hostname", "") - - response = jira_project_issue_summary( - email, api_token, project_key, cloud_hostname - ) - if "error" in response: - return Response(response, status=status.HTTP_400_BAD_REQUEST) - else: - return Response( - response, - status=status.HTTP_200_OK, - ) - return Response( - {"error": "Service not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class ImportServiceEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def post(self, request, slug, service): - project_id = request.data.get("project_id", False) - - if not project_id: - return Response( - {"error": "Project ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - if service == "github": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - if not data or not metadata or not config: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - if service == "jira": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - - cloud_hostname = metadata.get("cloud_hostname", False) - - if not cloud_hostname: - return Response( - {"error": "Cloud hostname is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not is_allowed_hostname(cloud_hostname): - return Response( - {"error": "Hostname is not a valid hostname."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not data or not metadata: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response( - {"error": "Servivce not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug): - imports = ( - Importer.objects.filter(workspace__slug=slug) - .order_by("-created_at") - .select_related("initiated_by", "project", "workspace") - ) - serializer = ImporterSerializer(imports, many=True) - return Response(serializer.data) - - def delete(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - - if importer.imported_data is not None: - # Delete all imported Issues - imported_issues = importer.imported_data.get("issues", []) - Issue.issue_objects.filter(id__in=imported_issues).delete() - - # Delete all imported Labels - imported_labels = importer.imported_data.get("labels", []) - Label.objects.filter(id__in=imported_labels).delete() - - if importer.service == "jira": - imported_modules = importer.imported_data.get("modules", []) - Module.objects.filter(id__in=imported_modules).delete() - importer.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - def patch(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - serializer = ImporterSerializer( - importer, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UpdateServiceImportStatusEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service, importer_id): - importer = Importer.objects.get( - pk=importer_id, - workspace__slug=slug, - project_id=project_id, - service=service, - ) - importer.status = request.data.get("status", "processing") - importer.save() - return Response(status.HTTP_200_OK) - - -class BulkImportIssuesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - # Get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - # Get the default state - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id, default=True - ).first() - # if there is no default state assign any random state - if default_state is None: - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id - ).first() - - # Get the maximum sequence_id - last_id = IssueSequence.objects.filter( - project_id=project_id - ).aggregate(largest=Max("sequence"))["largest"] - - last_id = 1 if last_id is None else last_id + 1 - - # Get the maximum sort order - largest_sort_order = Issue.objects.filter( - project_id=project_id, state=default_state - ).aggregate(largest=Max("sort_order"))["largest"] - - largest_sort_order = ( - 65535 if largest_sort_order is None else largest_sort_order + 10000 - ) - - # Get the issues_data - issues_data = request.data.get("issues_data", []) - - if not len(issues_data): - return Response( - {"error": "Issue data is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Issues - bulk_issues = [] - for issue_data in issues_data: - bulk_issues.append( - Issue( - project_id=project_id, - workspace_id=project.workspace_id, - state_id=issue_data.get("state") - if issue_data.get("state", False) - else default_state.id, - name=issue_data.get("name", "Issue Created through Bulk"), - description_html=issue_data.get( - "description_html", "

" - ), - description_stripped=( - None - if ( - issue_data.get("description_html") == "" - or issue_data.get("description_html") is None - ) - else strip_tags(issue_data.get("description_html")) - ), - sequence_id=last_id, - sort_order=largest_sort_order, - start_date=issue_data.get("start_date", None), - target_date=issue_data.get("target_date", None), - priority=issue_data.get("priority", "none"), - created_by=request.user, - ) - ) - - largest_sort_order = largest_sort_order + 10000 - last_id = last_id + 1 - - issues = Issue.objects.bulk_create( - bulk_issues, - batch_size=100, - ignore_conflicts=True, - ) - - # Sequences - _ = IssueSequence.objects.bulk_create( - [ - IssueSequence( - issue=issue, - sequence=issue.sequence_id, - project_id=project_id, - workspace_id=project.workspace_id, - ) - for issue in issues - ], - batch_size=100, - ) - - # Attach Labels - bulk_issue_labels = [] - for issue, issue_data in zip(issues, issues_data): - labels_list = issue_data.get("labels_list", []) - bulk_issue_labels = bulk_issue_labels + [ - IssueLabel( - issue=issue, - label_id=label_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for label_id in labels_list - ] - - _ = IssueLabel.objects.bulk_create( - bulk_issue_labels, batch_size=100, ignore_conflicts=True - ) - - # Attach Assignees - bulk_issue_assignees = [] - for issue, issue_data in zip(issues, issues_data): - assignees_list = issue_data.get("assignees_list", []) - bulk_issue_assignees = bulk_issue_assignees + [ - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for assignee_id in assignees_list - ] - - _ = IssueAssignee.objects.bulk_create( - bulk_issue_assignees, batch_size=100, ignore_conflicts=True - ) - - # Track the issue activities - IssueActivity.objects.bulk_create( - [ - IssueActivity( - issue=issue, - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - comment=f"imported the issue from {service}", - verb="created", - created_by=request.user, - ) - for issue in issues - ], - batch_size=100, - ) - - # Create Comments - bulk_issue_comments = [] - for issue, issue_data in zip(issues, issues_data): - comments_list = issue_data.get("comments_list", []) - bulk_issue_comments = bulk_issue_comments + [ - IssueComment( - issue=issue, - comment_html=comment.get("comment_html", "

"), - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for comment in comments_list - ] - - _ = IssueComment.objects.bulk_create( - bulk_issue_comments, batch_size=100 - ) - - # Attach Links - _ = IssueLink.objects.bulk_create( - [ - IssueLink( - issue=issue, - url=issue_data.get("link", {}).get( - "url", "https://github.com" - ), - title=issue_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue, issue_data in zip(issues, issues_data) - ] - ) - - return Response( - {"issues": IssueFlatSerializer(issues, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class BulkImportModulesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - modules_data = request.data.get("modules_data", []) - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - modules = Module.objects.bulk_create( - [ - Module( - name=module.get("name", uuid.uuid4().hex), - description=module.get("description", ""), - start_date=module.get("start_date", None), - target_date=module.get("target_date", None), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module in modules_data - ], - batch_size=100, - ignore_conflicts=True, - ) - - modules = Module.objects.filter( - id__in=[module.id for module in modules] - ) - - if len(modules) == len(modules_data): - _ = ModuleLink.objects.bulk_create( - [ - ModuleLink( - module=module, - url=module_data.get("link", {}).get( - "url", "https://plane.so" - ), - title=module_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module, module_data in zip(modules, modules_data) - ], - batch_size=100, - ignore_conflicts=True, - ) - - bulk_module_issues = [] - for module, module_data in zip(modules, modules_data): - module_issues_list = module_data.get("module_issues_list", []) - bulk_module_issues = bulk_module_issues + [ - ModuleIssue( - issue_id=issue, - module=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue in module_issues_list - ] - - _ = ModuleIssue.objects.bulk_create( - bulk_module_issues, batch_size=100, ignore_conflicts=True - ) - - serializer = ModuleSerializer(modules, many=True) - return Response( - {"modules": serializer.data}, status=status.HTTP_201_CREATED - ) - - else: - return Response( - { - "message": "Modules created but issues could not be imported" - }, - status=status.HTTP_200_OK, - ) diff --git a/apiserver/plane/app/views/integration/__init__.py b/apiserver/plane/app/views/integration/__init__.py deleted file mode 100644 index ea20d96eafd..00000000000 --- a/apiserver/plane/app/views/integration/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base import IntegrationViewSet, WorkspaceIntegrationViewSet -from .github import ( - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, -) -from .slack import SlackProjectSyncViewSet diff --git a/apiserver/plane/app/views/integration/base.py b/apiserver/plane/app/views/integration/base.py deleted file mode 100644 index d757fe47126..00000000000 --- a/apiserver/plane/app/views/integration/base.py +++ /dev/null @@ -1,181 +0,0 @@ -# Python improts -import uuid -import requests - -# Django imports -from django.contrib.auth.hashers import make_password - -# Third party imports -from rest_framework.response import Response -from rest_framework import status -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet -from plane.db.models import ( - Integration, - WorkspaceIntegration, - Workspace, - User, - WorkspaceMember, - APIToken, -) -from plane.app.serializers import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, -) -from plane.utils.integrations.github import ( - get_github_metadata, - delete_github_installation, -) -from plane.app.permissions import WorkSpaceAdminPermission -from plane.utils.integrations.slack import slack_oauth - - -class IntegrationViewSet(BaseViewSet): - serializer_class = IntegrationSerializer - model = Integration - - def create(self, request): - serializer = IntegrationSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IntegrationSerializer( - integration, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceIntegrationViewSet(BaseViewSet): - serializer_class = WorkspaceIntegrationSerializer - model = WorkspaceIntegration - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("integration") - ) - - def create(self, request, slug, provider): - workspace = Workspace.objects.get(slug=slug) - integration = Integration.objects.get(provider=provider) - config = {} - if provider == "github": - installation_id = request.data.get("installation_id", None) - if not installation_id: - return Response( - {"error": "Installation ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - metadata = get_github_metadata(installation_id) - config = {"installation_id": installation_id} - - if provider == "slack": - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - metadata = slack_response - access_token = metadata.get("access_token", False) - team_id = metadata.get("team", {}).get("id", False) - if not metadata or not access_token or not team_id: - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - config = {"team_id": team_id, "access_token": access_token} - - # Create a bot user - bot_user = User.objects.create( - email=f"{uuid.uuid4().hex}@plane.so", - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_bot=True, - first_name=integration.title, - avatar=integration.avatar_url - if integration.avatar_url is not None - else "", - ) - - # Create an API Token for the bot user - api_token = APIToken.objects.create( - user=bot_user, - user_type=1, # bot user - workspace=workspace, - ) - - workspace_integration = WorkspaceIntegration.objects.create( - workspace=workspace, - integration=integration, - actor=bot_user, - api_token=api_token, - metadata=metadata, - config=config, - ) - - # Add bot user as a member of workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_integration.workspace, - member=bot_user, - role=20, - ) - return Response( - WorkspaceIntegrationSerializer(workspace_integration).data, - status=status.HTTP_201_CREATED, - ) - - def destroy(self, request, slug, pk): - workspace_integration = WorkspaceIntegration.objects.get( - pk=pk, workspace__slug=slug - ) - - if workspace_integration.integration.provider == "github": - installation_id = workspace_integration.config.get( - "installation_id", False - ) - if installation_id: - delete_github_installation(installation_id=installation_id) - - workspace_integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/integration/github.py b/apiserver/plane/app/views/integration/github.py deleted file mode 100644 index 2d37c64b078..00000000000 --- a/apiserver/plane/app/views/integration/github.py +++ /dev/null @@ -1,202 +0,0 @@ -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - GithubIssueSync, - GithubRepositorySync, - GithubRepository, - WorkspaceIntegration, - ProjectMember, - Label, - GithubCommentSync, - Project, -) -from plane.app.serializers import ( - GithubIssueSyncSerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, -) -from plane.utils.integrations.github import get_github_repos -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) - - -class GithubRepositoriesEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug, workspace_integration_id): - page = request.GET.get("page", 1) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - if workspace_integration.integration.provider != "github": - return Response( - {"error": "Not a github integration"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - access_tokens_url = workspace_integration.metadata["access_tokens_url"] - repositories_url = ( - workspace_integration.metadata["repositories_url"] - + f"?per_page=100&page={page}" - ) - repositories = get_github_repos(access_tokens_url, repositories_url) - return Response(repositories, status=status.HTTP_200_OK) - - -class GithubRepositorySyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - - serializer_class = GithubRepositorySyncSerializer - model = GithubRepositorySync - - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - name = request.data.get("name", False) - url = request.data.get("url", False) - config = request.data.get("config", {}) - repository_id = request.data.get("repository_id", False) - owner = request.data.get("owner", False) - - if not name or not url or not repository_id or not owner: - return Response( - {"error": "Name, url, repository_id and owner are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace integration - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - GithubRepository.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=project_id, - ) - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", - project_id=project_id, - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - - # Create repo sync - repo_sync = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=request.data.get("credentials", {}), - project_id=project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id - ) - - # Return Response - return Response( - GithubRepositorySyncSerializer(repo_sync).data, - status=status.HTTP_201_CREATED, - ) - - -class GithubIssueSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubIssueSyncSerializer - model = GithubIssueSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - repository_sync_id=self.kwargs.get("repo_sync_id"), - ) - - -class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): - def post(self, request, slug, project_id, repo_sync_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - github_issue_syncs = request.data.get("github_issue_syncs", []) - github_issue_syncs = GithubIssueSync.objects.bulk_create( - [ - GithubIssueSync( - issue_id=github_issue_sync.get("issue"), - repo_issue_id=github_issue_sync.get("repo_issue_id"), - issue_url=github_issue_sync.get("issue_url"), - github_issue_id=github_issue_sync.get("github_issue_id"), - repository_sync_id=repo_sync_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for github_issue_sync in github_issue_syncs - ], - batch_size=100, - ignore_conflicts=True, - ) - - serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class GithubCommentSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubCommentSyncSerializer - model = GithubCommentSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_sync_id=self.kwargs.get("issue_sync_id"), - ) diff --git a/apiserver/plane/app/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py deleted file mode 100644 index c22ee3e52bd..00000000000 --- a/apiserver/plane/app/views/integration/slack.py +++ /dev/null @@ -1,96 +0,0 @@ -# Django import -from django.db import IntegrityError - -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - SlackProjectSync, - WorkspaceIntegration, - ProjectMember, -) -from plane.app.serializers import SlackProjectSyncSerializer -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) -from plane.utils.integrations.slack import slack_oauth - - -class SlackProjectSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - serializer_class = SlackProjectSyncSerializer - model = SlackProjectSync - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - try: - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id, workspace__slug=slug - ) - slack_project_sync = SlackProjectSync.objects.create( - access_token=slack_response.get("access_token"), - scopes=slack_response.get("scope"), - bot_user_id=slack_response.get("bot_user_id"), - webhook_url=slack_response.get("incoming_webhook", {}).get( - "url" - ), - data=slack_response, - team_id=slack_response.get("team", {}).get("id"), - team_name=slack_response.get("team", {}).get("name"), - workspace_integration=workspace_integration, - project_id=project_id, - ) - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=project_id, - ) - serializer = SlackProjectSyncSerializer(slack_project_sync) - return Response(serializer.data, status=status.HTTP_200_OK) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "Slack is already installed for the project"}, - status=status.HTTP_410_GONE, - ) - capture_exception(e) - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py deleted file mode 100644 index 7a1dc4fc6d2..00000000000 --- a/apiserver/plane/bgtasks/importer_task.py +++ /dev/null @@ -1,201 +0,0 @@ -# Python imports -import json -import requests -import uuid - -# Django imports -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.contrib.auth.hashers import make_password - -# Third Party imports -from celery import shared_task -from sentry_sdk import capture_exception - -# Module imports -from plane.app.serializers import ImporterSerializer -from plane.db.models import ( - Importer, - WorkspaceMember, - GithubRepositorySync, - GithubRepository, - ProjectMember, - WorkspaceIntegration, - Label, - User, - IssueProperty, - UserNotificationPreference, -) - - -@shared_task -def service_importer(service, importer_id): - try: - importer = Importer.objects.get(pk=importer_id) - importer.status = "processing" - importer.save() - - users = importer.data.get("users", []) - - # Check if we need to import users as well - if len(users): - # For all invited users create the users - new_users = User.objects.bulk_create( - [ - User( - email=user.get("email").strip().lower(), - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - ) - for user in users - if user.get("import", False) == "invite" - ], - batch_size=100, - ignore_conflicts=True, - ) - - _ = UserNotificationPreference.objects.bulk_create( - [UserNotificationPreference(user=user) for user in new_users], - batch_size=100, - ) - - workspace_users = User.objects.filter( - email__in=[ - user.get("email").strip().lower() - for user in users - if user.get("import", False) == "invite" - or user.get("import", False) == "map" - ] - ) - - # Check if any of the users are already member of workspace - _ = WorkspaceMember.objects.filter( - member__in=[user for user in workspace_users], - workspace_id=importer.workspace_id, - ).update(is_active=True) - - # Add new users to Workspace and project automatically - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - member=user, - workspace_id=importer.workspace_id, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - member=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - user=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - # Check if sync config is on for github importers - if service == "github" and importer.config.get("sync", False): - name = importer.metadata.get("name", False) - url = importer.metadata.get("url", False) - config = importer.metadata.get("config", {}) - owner = importer.metadata.get("owner", False) - repository_id = importer.metadata.get("repository_id", False) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace_id=importer.workspace_id, - integration__provider="github", - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=importer.project_id - ).delete() - GithubRepository.objects.filter( - project_id=importer.project_id - ).delete() - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", project_id=importer.project_id - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=importer.project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=importer.project_id, - ) - - # Create repo sync - _ = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=importer.data.get("credentials", {}), - project_id=importer.project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=importer.project_id, - ) - - if settings.PROXY_BASE_URL: - headers = {"Content-Type": "application/json"} - import_data_json = json.dumps( - ImporterSerializer(importer).data, - cls=DjangoJSONEncoder, - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/", - json=import_data_json, - headers=headers, - ) - - return - except Exception as e: - importer = Importer.objects.get(pk=importer_id) - importer.status = "failed" - importer.save() - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) - capture_exception(e) - return diff --git a/apiserver/plane/db/models/social_connection.py b/apiserver/plane/db/models/social_connection.py index 938a73a627a..73028e4198c 100644 --- a/apiserver/plane/db/models/social_connection.py +++ b/apiserver/plane/db/models/social_connection.py @@ -10,7 +10,7 @@ class SocialLoginConnection(BaseModel): medium = models.CharField( max_length=20, - choices=(("Google", "google"), ("Github", "github")), + choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")), default=None, ) last_login_at = models.DateTimeField(default=timezone.now, null=True) diff --git a/apiserver/plane/utils/importers/__init__.py b/apiserver/plane/utils/importers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apiserver/plane/utils/importers/jira.py b/apiserver/plane/utils/importers/jira.py deleted file mode 100644 index 6f3a7c21783..00000000000 --- a/apiserver/plane/utils/importers/jira.py +++ /dev/null @@ -1,117 +0,0 @@ -import requests -import re -from requests.auth import HTTPBasicAuth -from sentry_sdk import capture_exception -from urllib.parse import urlparse, urljoin - - -def is_allowed_hostname(hostname): - allowed_domains = [ - "atl-paas.net", - "atlassian.com", - "atlassian.net", - "jira.com", - ] - parsed_uri = urlparse(f"https://{hostname}") - domain = parsed_uri.netloc.split(":")[0] # Ensures no port is included - base_domain = ".".join(domain.split(".")[-2:]) - return base_domain in allowed_domains - - -def is_valid_project_key(project_key): - if project_key: - project_key = project_key.strip().upper() - # Adjust the regular expression as needed based on your specific requirements. - if len(project_key) > 30: - return False - # Check the validity of the key as well - pattern = re.compile(r"^[A-Z0-9]{1,10}$") - return pattern.match(project_key) is not None - else: - False - - -def generate_valid_project_key(project_key): - return project_key.strip().upper() - - -def generate_url(hostname, path): - if not is_allowed_hostname(hostname): - raise ValueError("Invalid or unauthorized hostname") - return urljoin(f"https://{hostname}", path) - - -def jira_project_issue_summary(email, api_token, project_key, hostname): - try: - if not is_allowed_hostname(hostname): - return {"error": "Invalid or unauthorized hostname"} - - if not is_valid_project_key(project_key): - return {"error": "Invalid project key"} - - auth = HTTPBasicAuth(email, api_token) - headers = {"Accept": "application/json"} - - # make the project key upper case - project_key = generate_valid_project_key(project_key) - - # issues - issue_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic", - ) - issue_response = requests.request( - "GET", issue_url, headers=headers, auth=auth - ).json()["total"] - - # modules - module_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic", - ) - module_response = requests.request( - "GET", module_url, headers=headers, auth=auth - ).json()["total"] - - # status - status_url = generate_url( - hostname, f"/rest/api/3/project/${project_key}/statuses" - ) - status_response = requests.request( - "GET", status_url, headers=headers, auth=auth - ).json() - - # labels - labels_url = generate_url( - hostname, f"/rest/api/3/label/?jql=project={project_key}" - ) - labels_response = requests.request( - "GET", labels_url, headers=headers, auth=auth - ).json()["total"] - - # users - users_url = generate_url( - hostname, f"/rest/api/3/users/search?jql=project={project_key}" - ) - users_response = requests.request( - "GET", users_url, headers=headers, auth=auth - ).json() - - return { - "issues": issue_response, - "modules": module_response, - "labels": labels_response, - "states": len(status_response), - "users": ( - [ - user - for user in users_response - if user.get("accountType") == "atlassian" - ] - ), - } - except Exception as e: - capture_exception(e) - return { - "error": "Something went wrong could not fetch information from jira" - } diff --git a/apiserver/plane/utils/integrations/__init__.py b/apiserver/plane/utils/integrations/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py deleted file mode 100644 index 5a7ce2aa29f..00000000000 --- a/apiserver/plane/utils/integrations/github.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import jwt -import requests -from urllib.parse import urlparse, parse_qs -from datetime import datetime, timedelta -from cryptography.hazmat.primitives.serialization import load_pem_private_key -from cryptography.hazmat.backends import default_backend -from django.conf import settings - - -def get_jwt_token(): - app_id = os.environ.get("GITHUB_APP_ID", "") - secret = bytes( - os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8" - ) - current_timestamp = int(datetime.now().timestamp()) - due_date = datetime.now() + timedelta(minutes=10) - expiry = int(due_date.timestamp()) - payload = { - "iss": app_id, - "sub": app_id, - "exp": expiry, - "iat": current_timestamp, - "aud": "https://github.com/login/oauth/access_token", - } - - priv_rsakey = load_pem_private_key(secret, None, default_backend()) - token = jwt.encode(payload, priv_rsakey, algorithm="RS256") - return token - - -def get_github_metadata(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.get(url, headers=headers).json() - return response - - -def get_github_repos(access_tokens_url, repositories_url): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token", "") - headers = { - "Authorization": "Bearer " + str(oauth_token), - "Accept": "application/vnd.github+json", - } - response = requests.get( - repositories_url, - headers=headers, - ).json() - return response - - -def delete_github_installation(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.delete(url, headers=headers) - return response - - -def get_github_repo_details(access_tokens_url, owner, repo): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token") - headers = { - "Authorization": "Bearer " + oauth_token, - "Accept": "application/vnd.github+json", - } - open_issues = requests.get( - f"https://api.github.com/repos/{owner}/{repo}", - headers=headers, - ).json()["open_issues_count"] - - total_labels = 0 - - labels_response = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/labels?per_page=100&page=1", - headers=headers, - ) - - # Check if there are more pages - if len(labels_response.links.keys()): - # get the query parameter of last - last_url = labels_response.links.get("last").get("url") - parsed_url = urlparse(last_url) - last_page_value = parse_qs(parsed_url.query)["page"][0] - total_labels = total_labels + 100 * (int(last_page_value) - 1) - - # Get labels in last page - last_page_labels = requests.get(last_url, headers=headers).json() - total_labels = total_labels + len(last_page_labels) - else: - total_labels = len(labels_response.json()) - - # Currently only supporting upto 100 collaborators - # TODO: Update this function to fetch all collaborators - collaborators = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/collaborators?per_page=100&page=1", - headers=headers, - ).json() - - return open_issues, total_labels, collaborators - - -def get_release_notes(): - token = settings.GITHUB_ACCESS_TOKEN - - if token: - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github.v3+json", - } - else: - headers = { - "Accept": "application/vnd.github.v3+json", - } - url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1" - response = requests.get(url, headers=headers) - - if response.status_code != 200: - return {"error": "Unable to render information from Github Repository"} - - return response.json() diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py deleted file mode 100644 index 0cc5b93b27e..00000000000 --- a/apiserver/plane/utils/integrations/slack.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import requests - - -def slack_oauth(code): - SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) - SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) - SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False) - - # Oauth Slack - if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET: - response = requests.get( - SLACK_OAUTH_URL, - params={ - "code": code, - "client_id": SLACK_CLIENT_ID, - "client_secret": SLACK_CLIENT_SECRET, - }, - ) - return response.json() - return {} From c16a5b9b71071bd6c2ebc2444dc3a417ad3e049a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:47:38 +0530 Subject: [PATCH 035/214] [WEB-626] chore: fix sentry issues and refactor issue actions logic for issue layouts (#3650) * restructure the logic to avoid throwing error if any dat is not found * updated files for previous commit * fix build errors * remove throwing error if userId is undefined * optionally chain display_name property to fix sentry issues * add ooptional check * change issue action logic to increase code maintainability and make sure to send only the updated date while updating the issue * fix issue updation bugs * fix module issues build error * fix runtime errors --- .../issues/peek-overview/layout.tsx | 1 - .../scope-and-demand/leaderboard.tsx | 8 +- .../core/sidebar/sidebar-progress-stats.tsx | 4 +- web/components/cycles/active-cycle-stats.tsx | 2 +- web/components/cycles/cycle-mobile-header.tsx | 46 +- .../dashboard/widgets/recent-activity.tsx | 2 +- .../collaborators-list.tsx | 6 +- web/components/headers/module-issues.tsx | 52 +- .../headers/project-view-issues.tsx | 46 +- .../integration/github/single-user-select.tsx | 31 +- .../integration/jira/import-users.tsx | 31 +- web/components/integration/single-import.tsx | 2 +- web/components/issues/issue-detail/root.tsx | 4 +- .../calendar/base-calendar-root.tsx | 85 +-- .../issue-layouts/calendar/calendar.tsx | 14 +- .../calendar/dropdowns/options-dropdown.tsx | 48 +- .../issues/issue-layouts/calendar/header.tsx | 17 +- .../calendar/roots/cycle-root.tsx | 33 +- .../calendar/roots/module-root.tsx | 43 +- .../calendar/roots/project-root.tsx | 44 +- .../calendar/roots/project-view-root.tsx | 21 +- .../issues/issue-layouts/calendar/utils.ts | 16 +- .../applied-filters/roots/cycle-root.tsx | 34 +- .../applied-filters/roots/module-root.tsx | 34 +- .../roots/project-view-root.tsx | 30 +- .../issue-layouts/gantt/base-gantt-root.tsx | 39 +- .../issues/issue-layouts/gantt/cycle-root.tsx | 38 +- .../issue-layouts/gantt/module-root.tsx | 38 +- .../issue-layouts/gantt/project-root.tsx | 27 +- .../issue-layouts/gantt/project-view-root.tsx | 25 +- .../issue-layouts/kanban/base-kanban-root.tsx | 91 +-- .../issues/issue-layouts/kanban/block.tsx | 17 +- .../issue-layouts/kanban/blocks-list.tsx | 7 +- .../issues/issue-layouts/kanban/default.tsx | 19 +- .../kanban/headers/group-by-card.tsx | 4 +- .../issue-layouts/kanban/kanban-group.tsx | 7 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 35 +- .../kanban/roots/draft-issue-root.tsx | 46 +- .../kanban/roots/module-root.tsx | 35 +- .../kanban/roots/profile-issues-root.tsx | 39 +- .../kanban/roots/project-root.tsx | 48 +- .../kanban/roots/project-view-root.tsx | 20 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 27 +- .../issues/issue-layouts/kanban/utils.ts | 54 +- .../issue-layouts/list/base-list-root.tsx | 87 +-- .../issues/issue-layouts/list/block.tsx | 11 +- .../issues/issue-layouts/list/blocks-list.tsx | 7 +- .../issues/issue-layouts/list/default.tsx | 19 +- .../list/headers/group-by-card.tsx | 4 +- .../list/roots/archived-issue-root.tsx | 31 +- .../issue-layouts/list/roots/cycle-root.tsx | 34 +- .../list/roots/draft-issue-root.tsx | 36 +- .../issue-layouts/list/roots/module-root.tsx | 35 +- .../list/roots/profile-issues-root.tsx | 37 +- .../issue-layouts/list/roots/project-root.tsx | 42 +- .../list/roots/project-view-root.tsx | 21 +- .../properties/all-properties.tsx | 159 ++--- .../quick-action-dropdowns/all-issue.tsx | 2 +- .../quick-action-dropdowns/cycle-issue.tsx | 2 +- .../quick-action-dropdowns/module-issue.tsx | 2 +- .../quick-action-dropdowns/project-issue.tsx | 2 +- .../roots/all-issue-layout-root.tsx | 52 +- .../roots/project-view-layout-root.tsx | 30 +- .../spreadsheet/base-spreadsheet-root.tsx | 98 +-- .../spreadsheet/issue-column.tsx | 8 +- .../issue-layouts/spreadsheet/issue-row.tsx | 15 +- .../spreadsheet/roots/cycle-root.tsx | 41 +- .../spreadsheet/roots/module-root.tsx | 38 +- .../spreadsheet/roots/project-root.tsx | 46 +- .../spreadsheet/roots/project-view-root.tsx | 21 +- .../spreadsheet/spreadsheet-table.tsx | 7 +- .../spreadsheet/spreadsheet-view.tsx | 7 +- web/components/issues/issue-modal/form.tsx | 14 +- web/components/issues/issue-modal/modal.tsx | 52 +- web/components/issues/peek-overview/root.tsx | 4 +- web/components/profile/overview/activity.tsx | 10 +- web/components/profile/sidebar.tsx | 17 +- web/components/project/member-list-item.tsx | 34 +- web/components/project/member-select.tsx | 4 +- web/components/project/project-logo.tsx | 4 +- .../project/send-project-invitation-modal.tsx | 49 +- .../confirm-workspace-member-remove.tsx | 4 +- web/helpers/issue.helper.ts | 17 + web/hooks/use-issues-actions.tsx | 576 ++++++++++++++++++ web/pages/profile/index.tsx | 10 +- web/store/issue/archived/issue.store.ts | 2 +- web/store/issue/cycle/filter.store.ts | 5 +- web/store/issue/cycle/issue.store.ts | 63 +- web/store/issue/issue-details/issue.store.ts | 2 +- web/store/issue/module/filter.store.ts | 5 +- web/store/issue/module/issue.store.ts | 55 +- web/store/issue/profile/filter.store.ts | 6 +- web/store/issue/profile/issue.store.ts | 50 +- web/store/issue/project-views/filter.store.ts | 6 +- web/store/issue/project-views/issue.store.ts | 70 +-- web/store/issue/workspace/filter.store.ts | 5 +- web/store/issue/workspace/issue.store.ts | 36 +- web/store/member/workspace-member.store.ts | 2 +- 98 files changed, 1402 insertions(+), 1864 deletions(-) create mode 100644 web/hooks/use-issues-actions.tsx diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 7345b4b28f9..602277f3ef4 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -11,7 +11,6 @@ import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overvie // lib import { useMobxStore } from "lib/mobx/store-provider"; - export const IssuePeekOverview: React.FC = observer(() => { // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); diff --git a/web/components/analytics/scope-and-demand/leaderboard.tsx b/web/components/analytics/scope-and-demand/leaderboard.tsx index 9cd38dde4ad..ae7447b0fdd 100644 --- a/web/components/analytics/scope-and-demand/leaderboard.tsx +++ b/web/components/analytics/scope-and-demand/leaderboard.tsx @@ -24,7 +24,7 @@ export const AnalyticsLeaderBoard: React.FC = ({ users, title, emptyState
{users.map((user) => ( = ({ users, title, emptyState {user.display_name
) : (
- {user.display_name !== "" ? user?.display_name?.[0] : "?"} + {user?.display_name !== "" ? user?.display_name?.[0] : "?"}
)} - {user.display_name !== "" ? `${user.display_name}` : "No assignee"} + {user?.display_name !== "" ? `${user?.display_name}` : "No assignee"}
{user.count} diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 157fd2c7918..6ff3d3f1e89 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -137,8 +137,8 @@ export const SidebarProgressStats: React.FC = ({ key={assignee.assignee_id} title={
- - {assignee.display_name} + + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 7d935c34779..0cf7449ae00 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -82,7 +82,7 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => {
- {assignee.display_name} + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index 942b5832b97..add78943c90 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -21,11 +21,7 @@ export const CycleMobileHeader = () => { { key: "calendar", title: "Calendar", icon: Calendar }, ]; - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId, cycleId } = router.query; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; // store hooks const { @@ -35,8 +31,14 @@ export const CycleMobileHeader = () => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -49,7 +51,7 @@ export const CycleMobileHeader = () => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !cycleId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -61,23 +63,41 @@ export const CycleMobileHeader = () => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); diff --git a/web/components/dashboard/widgets/recent-activity.tsx b/web/components/dashboard/widgets/recent-activity.tsx index 56056bce090..6e5c61355c3 100644 --- a/web/components/dashboard/widgets/recent-activity.tsx +++ b/web/components/dashboard/widgets/recent-activity.tsx @@ -71,7 +71,7 @@ export const RecentActivityWidget: React.FC = observer((props) => {

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( diff --git a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx index cfe7dd5caa9..1e796eea2e1 100644 --- a/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -100,14 +100,14 @@ export const CollaboratorsList: React.FC = (props) => { updateIsLoading?.(false); updateTotalPages(widgetStats.total_pages); - updateResultsCount(widgetStats.results.length); + updateResultsCount(widgetStats.results?.length); }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); - if (!widgetStats) return ; + if (!widgetStats || !widgetStats?.results) return ; return ( <> - {widgetStats?.results.map((user) => ( + {widgetStats?.results?.map((user) => ( { const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { - issuesFilter: { issueFilters, updateFilters }, + issuesFilter: { issueFilters }, } = useIssues(EIssuesStoreType.MODULE); + const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE); const { projectModuleIds, getModuleById } = useModule(); const { commandPalette: { toggleCreateIssueModal }, @@ -95,21 +97,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { layout: layout }, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -121,43 +117,25 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.FILTERS, - { [key]: newValues }, - moduleId?.toString() - ); + updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues }); }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + [projectId, moduleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_PROPERTIES, - property, - moduleId?.toString() - ); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); // derived values diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 4abc3edf944..ab3959716b4 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -33,11 +33,7 @@ import { ProjectLogo } from "components/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -61,15 +57,21 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -81,23 +83,41 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); diff --git a/web/components/integration/github/single-user-select.tsx b/web/components/integration/github/single-user-select.tsx index 24bd677d08f..2a323e72e12 100644 --- a/web/components/integration/github/single-user-select.tsx +++ b/web/components/integration/github/single-user-select.tsx @@ -44,16 +44,27 @@ export const SingleUserSelect: React.FC = ({ collaborator, index, users, workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null ); - const options = members?.map((member) => ({ - value: member.member.display_name, - query: member.member.display_name ?? "", - content: ( -

- - {member.member.display_name} -
- ), - })); + const options = members + ?.map((member) => { + if (!member?.member) return; + return { + value: member.member?.display_name, + query: member.member?.display_name ?? "", + content: ( +
+ + {member.member?.display_name} +
+ ), + }; + }) + .filter((member) => !!member) as + | { + value: string; + query: string; + content: JSX.Element; + }[] + | undefined; return (
diff --git a/web/components/integration/jira/import-users.tsx b/web/components/integration/jira/import-users.tsx index 584ba9feeea..f5d1221aec5 100644 --- a/web/components/integration/jira/import-users.tsx +++ b/web/components/integration/jira/import-users.tsx @@ -33,16 +33,27 @@ export const JiraImportUsers: FC = () => { workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug?.toString() ?? "") : null ); - const options = members?.map((member) => ({ - value: member.member.email, - query: member.member.display_name ?? "", - content: ( -
- - {member.member.display_name} -
- ), - })); + const options = members + ?.map((member) => { + if (!member?.member) return; + return { + value: member.member.email, + query: member.member.display_name ?? "", + content: ( +
+ + {member.member.display_name} +
+ ), + }; + }) + .filter((member) => !!member) as + | { + value: string; + query: string; + content: JSX.Element; + }[] + | undefined; return (
diff --git a/web/components/integration/single-import.tsx b/web/components/integration/single-import.tsx index 6d1d925e9b5..5d83a92c971 100644 --- a/web/components/integration/single-import.tsx +++ b/web/components/integration/single-import.tsx @@ -40,7 +40,7 @@ export const SingleImport: React.FC = ({ service, refreshing, handleDelet
{renderFormattedDate(service.created_at)}| - Imported by {service.initiated_by_detail.display_name} + Imported by {service.initiated_by_detail?.display_name}
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index f714a527931..5e56170a8f1 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -217,10 +217,10 @@ export const IssueDetailRoot: FC = observer((props) => { message: () => "Cycle remove from issue failed", }, }); - const response = await removeFromCyclePromise; + await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, + payload: { issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", change_details: "", diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index ab47a7399df..a36b8cc479d 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -1,34 +1,30 @@ -import { FC, useCallback } from "react"; +import { FC } from "react"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // components import { TOAST_TYPE, setToast } from "@plane/ui"; import { CalendarChart } from "components/issues"; +// hooks +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // ui // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TGroupedIssues, TIssue } from "@plane/types"; +import { TGroupedIssues } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; import { handleDragDrop } from "./utils"; -import { useIssues, useUser } from "hooks/store"; import { EUserProjectRoles } from "constants/project"; +type CalendarStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; + interface IBaseCalendarRoot { - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; - issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: CalendarStoreType; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; @@ -36,10 +32,8 @@ interface IBaseCalendarRoot { export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { - issueStore, - issuesFilterStore, QuickActions, - issueActions, + storeType, addIssuesToView, viewId, isCompletedCycle = false, @@ -50,16 +44,18 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { workspaceSlug, projectId } = router.query; // hooks - const { issueMap } = useIssues(); const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter, issueMap } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const displayFilters = issuesFilterStore.issueFilters?.displayFilters; + const displayFilters = issuesFilter.issueFilters?.displayFilters; - const groupedIssueIds = (issueStore.groupedIssueIds ?? {}) as TGroupedIssues; + const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; const onDragEnd = async (result: DropResult) => { if (!result) return; @@ -76,10 +72,9 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { result.destination, workspaceSlug?.toString(), projectId?.toString(), - issueStore, issueMap, groupedIssueIds, - viewId + updateIssue ).catch((err) => { setToast({ title: "Error", @@ -90,21 +85,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { } }; - const handleIssues = useCallback( - async (date: string, issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - return ( <>
{ handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] - ? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE) - : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE) - : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE) - : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.RESTORE) - : undefined + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => + removeIssueFromView && removeIssueFromView(issue.project_id, issue.id) } + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> )} addIssuesToView={addIssuesToView} - quickAddCallback={issueStore.quickAddIssue} + quickAddCallback={issues.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} + updateFilters={updateFilters} />
diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 30839326736..823866d9864 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -5,8 +5,10 @@ import { observer } from "mobx-react-lite"; import { Spinner } from "@plane/ui"; import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TGroupedIssues, TIssue, TIssueKanbanFilters, TIssueMap } from "@plane/types"; +import { ICalendarWeek } from "./types"; // constants -import { EIssuesStoreType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; import { useCalendarView } from "hooks/store/use-calendar-view"; @@ -14,8 +16,6 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; -import { ICalendarWeek } from "./types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -33,6 +33,11 @@ type Props = { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; }; export const CalendarChart: React.FC = observer((props) => { @@ -46,6 +51,7 @@ export const CalendarChart: React.FC = observer((props) => { quickAddCallback, addIssuesToView, viewId, + updateFilters, readOnly = false, } = props; // store hooks @@ -74,7 +80,7 @@ export const CalendarChart: React.FC = observer((props) => { return ( <>
- +
diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 6d00253dac2..d483ebe9164 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -9,6 +9,7 @@ import { Popover, Transition } from "@headlessui/react"; import { Check, ChevronUp } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TCalendarLayouts, TIssueKanbanFilters } from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; import { EIssueFilterType } from "constants/issue"; @@ -17,18 +18,21 @@ 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"; -import { TCalendarLayouts } from "@plane/types"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarOptionsDropdown: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { projectId } = router.query; const issueCalendarView = useCalendarView(); @@ -51,20 +55,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; const handleLayoutChange = (layout: TCalendarLayouts) => { - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - layout, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + layout, }, - viewId - ); + }); issueCalendarView.updateCalendarPayload( layout === "month" @@ -76,20 +74,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const handleToggleWeekends = () => { const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - show_weekends: !showWeekends, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + show_weekends: !showWeekends, }, - viewId - ); + }); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index 6129e451b50..aa055534dc5 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -9,14 +9,25 @@ 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"; +import { EIssueFilterType } from "constants/issue"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssueKanbanFilters, +} from "@plane/types"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const issueCalendarView = useCalendarView(); @@ -101,7 +112,7 @@ export const CalendarHeader: React.FC = observer((props) => { > Today - +
); diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 80a21838d18..128c84ba595 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; //hooks @@ -7,40 +7,15 @@ import { useCycle, useIssues } from "hooks/store"; import { CycleIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { EIssuesStoreType } from "constants/issue"; export const CycleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !projectId) return; - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId, projectId] - ); + const { issues } = useIssues(EIssuesStoreType.CYCLE); if (!cycleId) return null; @@ -57,13 +32,11 @@ export const CycleCalendarLayout: React.FC = observer(() => { return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index b2e2769ee9f..f6080630f94 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,47 +1,22 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query ; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const {issues} = useIssues(EIssuesStoreType.MODULE) + + if (!moduleId) return null; const addIssuesToView = useCallback( (issueIds: string[]) => { @@ -53,12 +28,10 @@ export const ModuleCalendarLayout: React.FC = observer(() => { return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index f8933a2271e..ad0cffe3344 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,48 +1,10 @@ -import { useMemo } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; -export const CalendarLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const CalendarLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index b50efd6c731..ff1b654e508 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -3,38 +3,21 @@ import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; // constants -export interface IViewCalendarLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewCalendarLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewCalendarLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts index 82d9ce0ce5f..fd96ff64764 100644 --- a/web/components/issues/issue-layouts/calendar/utils.ts +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -1,21 +1,16 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IModuleIssues } from "store/issue/module"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { TGroupedIssues, IIssueMap } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TIssue } from "@plane/types"; export const handleDragDrop = async ( source: DraggableLocation, destination: DraggableLocation, workspaceSlug: string | undefined, projectId: string | undefined, - store: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues, issueMap: IIssueMap, issueWithIds: TGroupedIssues, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise ) => { - if (!issueMap || !issueWithIds || !workspaceSlug || !projectId) return; + if (!issueMap || !issueWithIds || !workspaceSlug || !projectId || !updateIssue) return; const sourceColumnId = source?.droppableId || null; const destinationColumnId = destination?.droppableId || null; @@ -31,12 +26,11 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - const updateIssue = { + const updatedIssue = { id: removedIssueDetail?.id, target_date: destinationColumnId, }; - if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + return await updateIssue(projectId, updatedIssue.id, updatedIssue); } }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 57e28240b36..6a741b73d7b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -11,11 +11,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId, cycleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -37,13 +33,13 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - cycleId + cycleId.toString() ); return; } @@ -52,13 +48,13 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - cycleId + cycleId.toString() ); }; @@ -68,11 +64,17 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, cycleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + cycleId.toString() + ); }; // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId) return null; return (
@@ -84,7 +86,11 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { states={projectStates} /> - +
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index d2c9ba7ed35..b49ddf4d6bc 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -11,11 +11,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -36,13 +32,13 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - moduleId + moduleId.toString() ); return; } @@ -51,13 +47,13 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - moduleId + moduleId.toString() ); }; @@ -67,11 +63,17 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + moduleId.toString() + ); }; // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (!workspaceSlug || !projectId || Object.keys(appliedFilters).length === 0) return null; return (
@@ -83,7 +85,11 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { states={projectStates} /> - +
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 278e19d650f..760d2e7e4ba 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -14,11 +14,7 @@ import { IIssueFilterOptions } from "@plane/types"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -39,16 +35,16 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; if (!value) { updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }, - viewId + viewId.toString() ); return; } @@ -57,23 +53,29 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { newValues = newValues.filter((val) => val !== value); updateFilters( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }, - viewId + viewId.toString() ); }; const handleClearAllFilters = () => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters }, + viewId.toString() + ); }; const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters); diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 1231a31c575..11f52db8087 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -7,44 +7,43 @@ import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { EUserProjectRoles } from "constants/project"; import { renderIssueBlocksStructure } from "helpers/issue.helper"; import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components // helpers // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { TIssue, TUnGroupedIssues } from "@plane/types"; // constants -import { EIssueActions } from "../types"; +import { EIssuesStoreType } from "constants/issue"; +type GanttStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseGanttRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; viewId?: string; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; + storeType: GanttStoreType; } export const BaseGanttRoot: React.FC = observer((props: IBaseGanttRoot) => { - const { issueFiltersStore, issueStore, viewId } = props; + const { viewId, storeType } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue } = useIssuesActions(storeType); // store hooks const { membership: { currentProjectRole }, } = useUser(); const { issueMap } = useIssues(); - const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters; + const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - const { enableIssueCreation } = issueStore?.viewFlags || {}; + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; + const { enableIssueCreation } = issues?.viewFlags || {}; - const issues = issueIds.map((id) => issueMap?.[id]); + const issuesArray = issueIds.map((id) => issueMap?.[id]); const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => { if (!workspaceSlug) return; @@ -52,7 +51,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const payload: any = { ...data }; if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder; - await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId); + updateIssue && (await updateIssue(issue.project_id, issue.id, payload)); }; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -64,7 +63,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan border={false} title="Issues" loaderTitle="Issues" - blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} + blocks={issues ? renderIssueBlocksStructure(issuesArray) : null} blockUpdateHandler={updateIssueBlockStructure} blockToRender={(data: TIssue) => } sidebarToRender={(props) => } @@ -75,7 +74,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan enableAddBlock={isAllowed} quickAdd={ enableIssueCreation && isAllowed ? ( - + ) : undefined } showAllBlocks diff --git a/web/components/issues/issue-layouts/gantt/cycle-root.tsx b/web/components/issues/issue-layouts/gantt/cycle-root.tsx index 4d255b64fc0..923845e7b12 100644 --- a/web/components/issues/issue-layouts/gantt/cycle-root.tsx +++ b/web/components/issues/issue-layouts/gantt/cycle-root.tsx @@ -2,47 +2,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useCycle, useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; export const CycleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const { fetchCycleDetails } = useCycle(); + const { cycleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !issue.id) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index 3311b6c6ad1..e14f1339ac0 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -2,47 +2,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues, useModule } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; export const ModuleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const { fetchModuleDetails } = useModule(); + const { moduleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId || !issue.id) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 1f9e560d3f1..90fcca14547 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -1,33 +1,8 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; -export const GanttLayout: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }; - - return ; -}); +export const GanttLayout: React.FC = observer(() =>( )); diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index cda2a1e53b3..80d5e047b05 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -2,36 +2,15 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; import { BaseGanttRoot } from "./base-gantt-root"; // constants // types -export interface IViewGanttLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewGanttLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewGanttLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index f4f2044366e..0a492b5f717 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -6,45 +6,31 @@ import { useRouter } from "next/router"; import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { DeleteIssueModal } from "components/issues"; import { ISSUE_DELETED } from "constants/event-tracker"; -import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // ui // types -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; import { handleDragDrop } from "./utils"; +export type KanbanStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.DRAFT + | EIssuesStoreType.PROFILE; export interface IBaseKanBanLayout { - issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; - issuesFilter: - | IProjectIssuesFilter - | IModuleIssuesFilter - | ICycleIssuesFilter - | IDraftIssuesFilter - | IProjectViewIssuesFilter - | IProfileIssuesFilter; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; showLoader?: boolean; viewId?: string; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; @@ -58,10 +44,7 @@ type KanbanDragState = { export const BaseKanBanRoot: React.FC = observer((props: IBaseKanBanLayout) => { const { - issues, - issuesFilter, QuickActions, - issueActions, showLoader, viewId, storeType, @@ -77,7 +60,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas membership: { currentProjectRole }, } = useUser(); const { captureIssueEvent } = useEventTracker(); - const { issueMap } = useIssues(); + const { issueMap, issuesFilter, issues } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); const issueIds = issues?.groupedIssueIds || []; @@ -148,12 +133,12 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas result.destination, workspaceSlug?.toString(), projectId?.toString(), - issues, sub_group_by, group_by, issueMap, issueIds, - viewId + updateIssue, + removeIssue ).catch((err) => { setToast({ title: "Error", @@ -165,55 +150,39 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas } }; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - const renderQuickActions = useCallback( (issue: TIssue, customActionButton?: React.ReactElement) => ( handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps - [issueActions, handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); const handleDeleteIssue = async () => { - if (!handleDragDrop) return; + if (!handleDragDrop || !dragState.draggedIssueId) return; await handleDragDrop( dragState.source, dragState.destination, workspaceSlug?.toString(), projectId?.toString(), - issues, sub_group_by, group_by, issueMap, issueIds, - viewId + updateIssue, + removeIssue ).finally(() => { - handleIssues(issueMap[dragState.draggedIssueId!], EIssueActions.DELETE); + const draggedIssue = issueMap[dragState.draggedIssueId!]; + removeIssue(draggedIssue.project_id, draggedIssue.id); setDeleteIssueModal(false); setDragState({}); captureIssueEvent({ @@ -229,14 +198,12 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); else kanbanFilters.push(value); - issuesFilter.updateFilters( - workspaceSlug.toString(), + updateFilters( projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { [toggle]: kanbanFilters, - }, - viewId + } ); } }; @@ -294,7 +261,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas displayProperties={displayProperties} sub_group_by={sub_group_by} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={renderQuickActions} handleKanbanFilters={handleKanbanFilters} kanbanFilters={kanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 55675dd39d7..dabecc491bb 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -12,7 +12,6 @@ import { IssueProperties } from "../properties/all-properties"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; // ui // types -import { EIssueActions } from "../types"; // helper interface IssueBlockProps { @@ -23,7 +22,7 @@ interface IssueBlockProps { isDragDisabled: boolean; draggableId: string; index: number; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -34,13 +33,13 @@ interface IssueBlockProps { interface IssueDetailsBlockProps { issue: TIssue; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; isReadOnly: boolean; } const KanbanIssueDetailsBlock: React.FC = observer((props: IssueDetailsBlockProps) => { - const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props; + const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props; // hooks const { getProjectIdentifierById } = useProject(); const { @@ -48,10 +47,6 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop } = useApplication(); const { setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - if (issueToUpdate) await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -95,7 +90,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop issue={issue} displayProperties={displayProperties} activeLayout="Kanban" - handleIssues={updateIssue} + updateIssue={updateIssue} isReadOnly={isReadOnly} /> @@ -111,7 +106,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { isDragDisabled, draggableId, index, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -159,7 +154,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index ff1c9287398..7a58a49334f 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -2,7 +2,6 @@ import { MutableRefObject, memo } from "react"; //types import { KanbanIssueBlock } from "components/issues"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; -import { EIssueActions } from "../types"; // components interface IssueBlocksListProps { @@ -13,7 +12,7 @@ interface IssueBlocksListProps { issueIds: string[]; displayProperties: IIssueDisplayProperties | undefined; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -29,7 +28,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueIds, displayProperties, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -54,7 +53,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueId={issueId} issuesMap={issuesMap} displayProperties={displayProperties} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} draggableId={draggableId} index={index} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index ece578058f7..394f5ef1843 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,7 +1,6 @@ import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; // constants -import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useCycle, @@ -26,11 +25,11 @@ import { TIssueKanbanFilters, } from "@plane/types"; // parent components -import { EIssueActions } from "../types"; import { getGroupByColumns } from "../utils"; // components import { HeaderGroupByCard } from "./headers/group-by-card"; import { KanbanGroup } from "./kanban-group"; +import { KanbanStoreType } from "./base-kanban-root"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -40,7 +39,7 @@ export interface IGroupByKanBan { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: any; @@ -53,7 +52,7 @@ export interface IGroupByKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -70,7 +69,7 @@ const GroupByKanBan: React.FC = observer((props) => { group_by, sub_group_id = "null", isDragDisabled, - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -164,7 +163,7 @@ const GroupByKanBan: React.FC = observer((props) => { group_by={group_by} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} quickAddCallback={quickAddCallback} @@ -190,7 +189,7 @@ export interface IKanBan { sub_group_by: string | null; group_by: string | null; sub_group_id?: string; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; @@ -204,7 +203,7 @@ export interface IKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -219,7 +218,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by, group_by, sub_group_id = "null", - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -246,7 +245,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by={sub_group_by} sub_group_id={sub_group_id} isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index b3cc24f289c..a14dd5ddcf6 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -9,11 +9,11 @@ import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal } from "components/issues"; // constants -import { TCreateModalStoreTypes } from "constants/issue"; // hooks import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; +import { KanbanStoreType } from "../base-kanban-root"; interface IHeaderGroupByCard { sub_group_by: string | null; @@ -26,7 +26,7 @@ interface IHeaderGroupByCard { handleKanbanFilters: any; issuePayload: Partial; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 9d7053216ae..48e92feba0e 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -12,7 +12,6 @@ import { TSubGroupedIssues, TUnGroupedIssues, } from "@plane/types"; -import { EIssueActions } from "../types"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { @@ -25,7 +24,7 @@ interface IKanbanGroup { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -53,7 +52,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds, peekIssueId, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, enableQuickIssueCreate, @@ -135,7 +134,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} displayProperties={displayProperties} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index c73b12afbf5..19ac8a1d982 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks @@ -7,8 +7,6 @@ import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // ui // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; @@ -19,35 +17,9 @@ export const CycleKanBanLayout: React.FC = observer(() => { const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -63,9 +35,6 @@ export const CycleKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query; - - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const DraftKanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 96cfaceda89..eaf96a9946d 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hook @@ -7,9 +7,7 @@ import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; export interface IModuleKanBanLayout {} @@ -19,39 +17,10 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const { workspaceSlug, projectId, moduleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { - membership: { currentWorkspaceAllProjectsRole }, - } = useUser(); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); + membership: { currentWorkspaceAllProjectsRole }, +} = useUser(); const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -52,9 +22,6 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const KanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 77689e563c5..c1a07c3172e 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -3,37 +3,19 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // constant // types -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -export interface IViewKanBanLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewKanBanLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewKanBanLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( void; + storeType: KanbanStoreType; } const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { @@ -43,6 +43,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, group_by, + storeType, list, kanbanFilters, handleKanbanFilters, @@ -62,6 +63,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} issuePayload={_list.payload} + storeType={storeType} />
))} @@ -73,13 +75,13 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; showEmptyGroup: boolean; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; enableQuickIssueCreate: boolean; canEditProperties: (projectId: string | undefined) => boolean; addIssuesToView?: (issueIds: string[]) => Promise; @@ -99,7 +101,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by, group_by, list, - handleIssues, + storeType, + updateIssue, quickActions, displayProperties, kanbanFilters, @@ -153,7 +156,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by={sub_group_by} group_by={group_by} sub_group_id={_list.id} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -165,6 +168,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { viewId={viewId} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + storeType={storeType} />
)} @@ -180,14 +184,14 @@ export interface IKanBanSwimLanes { displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; enableQuickIssueCreate: boolean; quickAddCallback?: ( @@ -208,7 +212,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties, sub_group_by, group_by, - handleIssues, + updateIssue, + storeType, quickActions, kanbanFilters, handleKanbanFilters, @@ -261,6 +266,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} list={groupByList} + storeType={storeType} />
@@ -272,7 +278,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties={displayProperties} group_by={group_by} sub_group_by={sub_group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -285,6 +291,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { quickAddCallback={quickAddCallback} viewId={viewId} scrollableContainerRef={scrollableContainerRef} + storeType={storeType} /> )}
diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index 61759852439..855f096e655 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,12 +1,5 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IDraftIssues } from "store/issue/draft"; -import { IModuleIssues } from "store/issue/module"; -import { IProfileIssues } from "store/issue/profile"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { IWorkspaceIssues } from "store/issue/workspace"; -import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { const sortOrderDefaultValue = 65535; @@ -48,24 +41,16 @@ export const handleDragDrop = async ( destination: DraggableLocation | null | undefined, workspaceSlug: string | undefined, projectId: string | undefined, // projectId for all views or user id in profile issues - store: - | IProjectIssues - | ICycleIssues - | IDraftIssues - | IModuleIssues - | IDraftIssues - | IProjectViewIssues - | IProfileIssues - | IWorkspaceIssues, subGroupBy: string | null, groupBy: string | null, issueMap: IIssueMap, issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, + removeIssue: (projectId: string, issueId: string) => Promise | undefined ) => { if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return; - let updateIssue: any = {}; + let updatedIssue: any = {}; const sourceDroppableId = source?.droppableId; const destinationDroppableId = destination?.droppableId; @@ -100,8 +85,7 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); if (removed) { - if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed); //, viewId); - else return await store?.removeIssue(workspaceSlug, projectId, removed); + return await removeIssue(projectId, removed); } } else { //spreading the array to stop changing the original reference @@ -118,14 +102,14 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - updateIssue = { + updatedIssue = { id: removedIssueDetail?.id, project_id: removedIssueDetail?.project_id, }; // for both horizontal and vertical dnd - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, ...handleSortOrder( sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues, destination.index, @@ -136,19 +120,19 @@ export const handleDragDrop = async ( if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) { if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) { if (sourceGroupByColumnId != destinationGroupByColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } else { if (subGroupBy === "state") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationSubGroupByColumnId, priority: destinationGroupByColumnId, }; if (subGroupBy === "priority") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationGroupByColumnId, priority: destinationSubGroupByColumnId, }; @@ -156,15 +140,13 @@ export const handleDragDrop = async ( } else { // for horizontal dnd if (sourceColumnId != destinationColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } - if (updateIssue && updateIssue?.id) { - if (viewId) - return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue); + if (updatedIssue && updatedIssue?.id) { + return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); } } }; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 8a3d87e403b..5777f4e70f5 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,68 +1,46 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; // types -import { TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; -import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; + +import { TIssue } from "@plane/types" // components import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; +import { useIssuesActions } from "hooks/use-issues-actions"; // constants // hooks +type ListStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.ARCHIVED + | EIssuesStoreType.DRAFT + | EIssuesStoreType.PROFILE; interface IBaseListRoot { - issuesFilter: - | IProjectIssuesFilter - | IModuleIssuesFilter - | ICycleIssuesFilter - | IProjectViewIssuesFilter - | IProfileIssuesFilter - | IDraftIssuesFilter - | IArchivedIssuesFilter; - issues: - | IProjectIssues - | ICycleIssues - | IModuleIssues - | IProjectViewIssues - | IProfileIssues - | IDraftIssues - | IArchivedIssues; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; viewId?: string; - storeType: TCreateModalStoreTypes; + storeType: ListStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } - export const BaseListRoot = observer((props: IBaseListRoot) => { const { - issuesFilter, - issues, QuickActions, - issueActions, viewId, storeType, addIssuesToView, canEditPropertiesBasedOnProject, isCompletedCycle = false, } = props; + + const { issuesFilter, issues } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); // mobx store const { membership: { currentProjectRole }, @@ -80,7 +58,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const isEditingAllowedBasedOnProject = canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; - return enableInlineEditing && isEditingAllowedBasedOnProject; + return !!enableInlineEditing && isEditingAllowedBasedOnProject; }, [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); @@ -91,37 +69,20 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const group_by = displayFilters?.group_by || null; const showEmptyGroup = displayFilters?.show_empty_groups ?? false; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - const renderQuickActions = useCallback( (issue: TIssue) => ( handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( @@ -130,7 +91,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { issuesMap={issueMap} displayProperties={displayProperties} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={renderQuickActions} issueIds={issueIds} showEmptyGroup={showEmptyGroup} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index a2148634c59..09913734863 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -9,19 +9,18 @@ import { useApplication, useIssueDetail, useProject } from "hooks/store"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; import { IssueProperties } from "../properties/all-properties"; -import { EIssueActions } from "../types"; interface IssueBlockProps { issueId: string; issuesMap: TIssueMap; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; canEditProperties: (projectId: string | undefined) => boolean; } export const IssueBlock: React.FC = observer((props: IssueBlockProps) => { - const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issuesMap, issueId, updateIssue, quickActions, displayProperties, canEditProperties } = props; // hooks const { router: { workspaceSlug }, @@ -29,10 +28,6 @@ export const IssueBlock: React.FC = observer((props: IssueBlock const { getProjectIdentifierById } = useProject(); const { peekIssue, setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -91,7 +86,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="relative flex items-center gap-2 whitespace-nowrap" issue={issue} isReadOnly={!canEditIssueProperties} - handleIssues={updateIssue} + updateIssue={updateIssue} displayProperties={displayProperties} activeLayout="List" /> diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 23c364b675d..2296e7b6807 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -4,20 +4,19 @@ import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; containerRef: MutableRefObject; } export const IssueBlocksList: FC = (props) => { - const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props; + const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props; return (
@@ -35,7 +34,7 @@ export const IssueBlocksList: FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; @@ -36,7 +35,7 @@ export interface IGroupByList { viewId?: string ) => Promise; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; @@ -47,7 +46,7 @@ const GroupByList: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, displayProperties, enableIssueQuickAdd, @@ -142,7 +141,7 @@ const GroupByList: React.FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; @@ -184,7 +183,7 @@ export interface IList { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; isCompletedCycle?: boolean; } @@ -194,7 +193,7 @@ export const List: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, quickAddCallback, viewId, @@ -214,7 +213,7 @@ export const List: React.FC = (props) => { issueIds={issueIds as TUnGroupedIssues} issuesMap={issuesMap} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} displayProperties={displayProperties} enableIssueQuickAdd={enableIssueQuickAdd} diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index acf26adb5f8..fa1a393c42e 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -10,7 +10,7 @@ import { CreateUpdateIssueModal } from "components/issues"; // ui // mobx // hooks -import { TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -21,7 +21,7 @@ interface IHeaderGroupByCard { count: number; issuePayload: Partial; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 2f3807beb21..73f8e3d3b7b 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,47 +1,20 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ArchivedIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseListRoot } from "../base-list-root"; export const ArchivedIssueListLayout: FC = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - const issueActions = useMemo( - () => ({ - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.RESTORE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.restoreIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - const canEditPropertiesBasedOnProject = () => false; return ( ); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 46ee7f32ecb..26afdf25ba6 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks @@ -7,9 +7,7 @@ import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseListRoot } from "../base-list-root"; export interface ICycleListLayout {} @@ -18,34 +16,9 @@ export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -61,10 +34,7 @@ export const CycleListLayout: React.FC = observer(() => { return ( { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index aca528a6a6a..3c6a8894a8e 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store @@ -7,8 +7,6 @@ import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; @@ -18,40 +16,11 @@ export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index dc0c68cd810..f24683d95c6 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,50 +1,20 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useUser } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; export const ProfileIssuesListLayout: FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); - const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -53,10 +23,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { return ( diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 8a0935979be..fbbd26ffb24 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,55 +1,19 @@ -import { FC, useMemo } from "react"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { ProjectIssueQuickActions } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; export const ListLayout: FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.archiveIssue(workspaceSlug, projectId, issue.id); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [issues] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 82ca03d4279..260dd54bd6e 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -3,29 +3,13 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // constants // types -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; // components import { BaseListRoot } from "../base-list-root"; -export interface IViewListLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewListLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); - +export const ProjectViewListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; @@ -33,10 +17,7 @@ export const ProjectViewListLayout: React.FC = observer((props) return ( diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index c3a6bc03766..8c1e33b8c19 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -30,7 +30,7 @@ import { WithDisplayPropertiesHOC } from "../properties/with-display-properties- export interface IIssueProperties { issue: TIssue; - handleIssues: (issue: TIssue) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; displayProperties: IIssueDisplayProperties | undefined; isReadOnly: boolean; className: string; @@ -38,7 +38,7 @@ export interface IIssueProperties { } export const IssueProperties: React.FC = observer((props) => { - const { issue, handleIssues, displayProperties, activeLayout, isReadOnly, className } = props; + const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props; // store hooks const { labelMap } = useLabel(); const { captureIssueEvent } = useEventTracker(); @@ -80,59 +80,63 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleState = (stateId: string) => { - handleIssues({ ...issue, state_id: stateId }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "state", - change_details: stateId, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "state", + change_details: stateId, + }, + }); }); - }); }; const handlePriority = (value: TIssuePriorities) => { - handleIssues({ ...issue, priority: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "priority", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "priority", + change_details: value, + }, + }); }); - }); }; const handleLabel = (ids: string[]) => { - handleIssues({ ...issue, label_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "labels", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "labels", + change_details: ids, + }, + }); }); - }); }; const handleAssignee = (ids: string[]) => { - handleIssues({ ...issue, assignee_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "assignees", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "assignees", + change_details: ids, + }, + }); }); - }); }; const handleModule = useCallback( @@ -175,45 +179,52 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleStartDate = (date: Date | null) => { - handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "start_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "start_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleTargetDate = (date: Date | null) => { - handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "target_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "target_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleEstimate = (value: number | null) => { - handleIssues({ ...issue, estimate_point: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "estimate_point", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "estimate_point", + change_details: value, + }, + }); }); - }); }; const redirectToIssueDetail = () => { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 5f7609a0389..f6c63191f11 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -90,7 +90,7 @@ export const AllIssueQuickActions: React.FC = observer((props }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 89beda00c99..fe713ed231b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -101,7 +101,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.CYCLE} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 26eb6997cf4..f24f6869efe 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -100,7 +100,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.MODULE} /> diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 33b73f88cfa..24a2433d54e 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -100,7 +100,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} isDraft={isDraftIssue} diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 1367eccc481..bcba7152e4e 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useMemo } from "react"; +import React, { Fragment, useCallback } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; @@ -6,6 +6,7 @@ import useSWR from "swr"; // hooks import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; @@ -14,7 +15,6 @@ import { EmptyState } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; -import { EIssueActions } from "../types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -30,8 +30,9 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue, archiveIssue }, + issues: { loader, groupedIssueIds, fetchIssues }, } = useIssues(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); const { dataViewId, issueIds } = groupedIssueIds; const { @@ -111,41 +112,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, globalViewId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await archiveIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [updateIssue, removeIssue, workspaceSlug] - ); - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); - if (action === EIssueActions.DELETE) await issueActions[action]!(issue); - if (action === EIssueActions.ARCHIVE) await issueActions[action]!(issue); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !globalViewId) return; @@ -166,14 +132,14 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleIssues({ ...issue }, EIssueActions.UPDATE)} - handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)} - handleArchive={async () => handleIssues(issue, EIssueActions.ARCHIVE)} + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!canEditProperties(issue.project_id)} /> ), - [canEditProperties, handleIssues] + [canEditProperties, removeIssue, updateIssue, archiveIssue] ); if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { @@ -213,7 +179,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleDisplayFilterUpdate={handleDisplayFiltersUpdate} issueIds={issueIds} quickActions={renderQuickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} viewId={globalViewId} /> diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index dbd6c5f96a7..d15e65865d2 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useMemo } from "react"; +import React, { Fragment } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -19,8 +19,6 @@ import { ActiveLoader } from "components/ui"; import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; export const ProjectViewLayoutRoot: React.FC = observer(() => { // router @@ -45,22 +43,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue, viewId?.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id, viewId?.toString()); - }, - }), - [issues, workspaceSlug, projectId, viewId] - ); - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; if (!workspaceSlug || !projectId || !viewId) return <>; @@ -81,15 +63,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
{activeLayout === "list" ? ( - + ) : activeLayout === "kanban" ? ( - + ) : activeLayout === "calendar" ? ( - + ) : activeLayout === "gantt_chart" ? ( - + ) : activeLayout === "spreadsheet" ? ( - + ) : null}
diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 5a522a527e4..fa89b77eda9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -2,56 +2,44 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { EIssueFilterType } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { useUser } from "hooks/store"; +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; // views // types // constants -import { ICycleIssuesFilter, ICycleIssues } from "store/issue/cycle"; -import { IModuleIssuesFilter, IModuleIssues } from "store/issue/module"; -import { IProjectIssuesFilter, IProjectIssues } from "store/issue/project"; -import { IProjectViewIssuesFilter, IProjectViewIssues } from "store/issue/project-views"; import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; import { SpreadsheetView } from "./spreadsheet-view"; +export type SpreadsheetStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseSpreadsheetRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | ICycleIssues | IModuleIssues | IProjectViewIssues; viewId?: string; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => void; - [EIssueActions.UPDATE]?: (issue: TIssue) => void; - [EIssueActions.REMOVE]?: (issue: TIssue) => void; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => void; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: SpreadsheetStoreType; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { - const { - issueFiltersStore, - issueStore, - viewId, - QuickActions, - issueActions, - canEditPropertiesBasedOnProject, - isCompletedCycle = false, - } = props; + const { viewId, QuickActions, storeType, canEditPropertiesBasedOnProject, isCompletedCycle = false } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { projectId } = router.query; // store hooks const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); // derived values - const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; + const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; // user role validation const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -65,32 +53,17 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - issueActions[action]!(issue); - } - }, - [issueActions] - ); + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; + if ( !projectId) return; - issueFiltersStore.updateFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_FILTERS, - { - ...updatedDisplayFilter, - }, - viewId - ); + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + ...updatedDisplayFilter, + }); }, - [issueFiltersStore, projectId, workspaceSlug, viewId] + [ projectId, updateFilters] ); const renderQuickActions = useCallback( @@ -98,37 +71,28 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!isEditingAllowed || isCompletedCycle} /> ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; isEstimateEnabled: boolean; }; export const IssueColumn = observer((props: Props) => { - const { displayProperties, issueDetail, disableUserActions, property, handleIssues, isEstimateEnabled } = props; + const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props; // router const router = useRouter(); const tableCellRef = useRef(null); @@ -44,7 +43,8 @@ export const IssueColumn = observer((props: Props) => { , updates: any) => - handleIssues({ ...issue, ...data }, EIssueActions.UPDATE).then(() => { + updateIssue && + updateIssue(issue.project_id, issue.id, data).then(() => { captureIssueEvent({ eventName: "Issue updated", payload: { diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 161aa07aec0..8a8ce29f410 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -18,7 +18,6 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; // local components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import { EIssueActions } from "../types"; import { IssueColumn } from "./issue-column"; interface Props { @@ -30,7 +29,7 @@ interface Props { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -46,7 +45,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -76,7 +75,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} isExpanded={isExpanded} @@ -96,7 +95,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel + 1} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} containerRef={containerRef} @@ -116,7 +115,7 @@ interface IssueRowDetailsProps { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -132,7 +131,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -261,7 +260,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { issueDetail={issueDetail} disableUserActions={disableUserActions} property={property} - handleIssues={handleIssues} + updateIssue={updateIssue} isEstimateEnabled={isEstimateEnabled} /> ))} diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 7a8977b2292..b8b4fd08a24 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,59 +1,32 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useCycle, useIssues } from "hooks/store"; +import { useCycle } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { CycleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const CycleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { cycleId } = router.query; const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, cycleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssueFromCycle(workspaceSlug, issue.project_id, cycleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); + if (!cycleId) return null; + return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index c52b40527d9..a95919cdce1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -1,51 +1,23 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + const { moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] - ); + if (!moduleId) return null; return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index cc570fd81b5..dc9d354a6d2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -1,48 +1,10 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; - -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -export const ProjectSpreadsheetLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const ProjectSpreadsheetLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index dd134e070c2..754d87c2fba 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -3,39 +3,22 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { EIssuesStoreType } from "constants/issue"; -import { useIssues } from "hooks/store"; // components -import { TIssue } from "@plane/types"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssueActions } from "../../types"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; // types // constants -export interface IViewSpreadsheetLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewSpreadsheetLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index 4bb2cbeaba0..896d5a4ddcc 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -3,7 +3,6 @@ import { observer } from "mobx-react-lite"; //types import { useTableKeyboardNavigation } from "hooks/use-table-keyboard-navigation"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; //components import { SpreadsheetIssueRow } from "./issue-row"; import { SpreadsheetHeader } from "./spreadsheet-header"; @@ -19,7 +18,7 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; containerRef: MutableRefObject; @@ -34,7 +33,7 @@ export const SpreadsheetTable = observer((props: Props) => { isEstimateEnabled, portalElement, quickActions, - handleIssues, + updateIssue, canEditProperties, containerRef, } = props; @@ -95,7 +94,7 @@ export const SpreadsheetTable = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={0} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} containerRef={containerRef} isScrolled={isScrolled} diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index f71634ab841..ed243d3127c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -5,7 +5,6 @@ import { Spinner } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues"; import { useProject } from "hooks/store"; import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssueActions } from "../types"; import { SpreadsheetTable } from "./spreadsheet-table"; // types //hooks @@ -20,7 +19,7 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( workspaceSlug: string, @@ -41,7 +40,7 @@ export const SpreadsheetView: React.FC = observer((props) => { handleDisplayFilterUpdate, issueIds, quickActions, - handleIssues, + updateIssue, quickAddCallback, viewId, canEditProperties, @@ -75,7 +74,7 @@ export const SpreadsheetView: React.FC = observer((props) => { isEstimateEnabled={isEstimateEnabled} portalElement={portalRef} quickActions={quickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} containerRef={containerRef} /> diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 03a9ae5b052..67d3c904d35 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -29,6 +29,7 @@ import { FileService } from "services/file.service"; // components // ui // helpers +import { getChangedIssuefields } from "helpers/issue.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -126,7 +127,7 @@ export const IssueFormRoot: FC = observer((props) => { } = useIssueDetail(); // form info const { - formState: { errors, isDirty, isSubmitting }, + formState: { errors, isDirty, isSubmitting, dirtyFields }, handleSubmit, reset, watch, @@ -166,7 +167,15 @@ export const IssueFormRoot: FC = observer((props) => { const issueName = watch("name"); const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { - await onSubmit(formData, is_draft_issue); + const submitData = !data?.id + ? formData + : { + ...getChangedIssuefields(formData, dirtyFields as { [key: string]: boolean | undefined }), + project_id: getValues("project_id"), + id: data.id, + description_html: formData.description_html ?? "

", + }; + await onSubmit(submitData, is_draft_issue); setGptAssistantModal(false); @@ -761,3 +770,4 @@ export const IssueFormRoot: FC = observer((props) => { ); }); + diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index fa6c4fbb337..b4cf05fc835 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { TOAST_TYPE, setToast } from "@plane/ui"; import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker"; -import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { EIssuesStoreType } from "constants/issue"; import { useApplication, useEventTracker, @@ -17,6 +17,7 @@ import { useIssueDetail, } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { useIssuesActions } from "hooks/use-issues-actions"; // components import type { TIssue } from "@plane/types"; import { DraftIssueLayout } from "./draft-issue-layout"; @@ -31,7 +32,7 @@ export interface IssuesModalProps { onClose: () => void; onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; - storeType?: TCreateModalStoreTypes; + storeType?: EIssuesStoreType; isDraft?: boolean; } @@ -53,41 +54,15 @@ export const CreateUpdateIssueModal: React.FC = observer((prop // store hooks const { captureIssueEvent } = useEventTracker(); const { - router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId }, + router: { workspaceSlug, projectId, cycleId, moduleId }, } = useApplication(); const { workspaceProjectIds } = useProject(); const { fetchCycleDetails } = useCycle(); const { fetchModuleDetails } = useModule(); - const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); const { issues: moduleIssues } = useIssues(EIssuesStoreType.MODULE); const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); - const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); const { fetchIssue } = useIssueDetail(); - // store mapping based on current store - const issueStores = { - [EIssuesStoreType.PROJECT]: { - store: projectIssues, - viewId: undefined, - }, - [EIssuesStoreType.PROJECT_VIEW]: { - store: viewIssues, - viewId: projectViewId, - }, - [EIssuesStoreType.PROFILE]: { - store: profileIssues, - viewId: undefined, - }, - [EIssuesStoreType.CYCLE]: { - store: cycleIssues, - viewId: cycleId, - }, - [EIssuesStoreType.MODULE]: { - store: moduleIssues, - viewId: moduleId, - }, - }; // router const router = useRouter(); // local storage @@ -95,7 +70,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop Record> >("draftedIssue", {}); // current store details - const { store: currentIssueStore, viewId } = issueStores[storeType]; + const { createIssue, updateIssue } = useIssuesActions(storeType); const fetchIssueDetail = async (issueId: string | undefined) => { if (!workspaceSlug) return; @@ -176,11 +151,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop try { const response = is_draft_issue ? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload) - : await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); + : createIssue && (await createIssue(payload.project_id, payload)); if (!response) throw new Error(); - currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId); - if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE) await addIssueToCycle(response, payload.cycle_id); if (payload.module_ids && payload.module_ids.length > 0 && storeType !== EIssuesStoreType.MODULE) @@ -217,7 +190,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop try { isDraft ? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload) - : await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); + : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); setToast({ type: TOAST_TYPE.SUCCESS, @@ -234,7 +207,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Issue could not be created. Please try again.", + message: "Issue could not be updated. Please try again.", }); captureIssueEvent({ eventName: ISSUE_UPDATED, @@ -244,13 +217,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop } }; - const handleFormSubmit = async (formData: Partial, is_draft_issue: boolean = false) => { - if (!workspaceSlug || !formData.project_id || !storeType) return; - - const payload: Partial = { - ...formData, - description_html: formData.description_html ?? "

", - }; + const handleFormSubmit = async (payload: Partial, is_draft_issue: boolean = false) => { + if (!workspaceSlug || !payload.project_id || !storeType) return; let response: TIssue | undefined = undefined; if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue); diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 3eae8d3e870..37cd8f37566 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -234,10 +234,10 @@ export const IssuePeekOverview: FC = observer((props) => { message: () => "Cycle remove from issue failed", }, }); - const response = await removeFromCyclePromise; + await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, + payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", change_details: "", diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 4a6cf98bedb..c8af1ccf8f2 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -46,24 +46,24 @@ export const ProfileActivity = observer(() => { {userProfileActivity.results.map((activity) => (
- {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( + {activity.actor_detail?.avatar && activity.actor_detail?.avatar !== "" ? ( {activity.actor_detail.display_name} ) : (
- {activity.actor_detail.display_name?.charAt(0)} + {activity.actor_detail?.display_name?.charAt(0)}
)}

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail?.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 3121f5b2ed4..adb9c63d721 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -96,21 +96,22 @@ export const ProfileSidebar = observer(() => { )} {userProjectsData.user_data.display_name}

- {userProjectsData.user_data.avatar && userProjectsData.user_data.avatar !== "" ? ( + {userProjectsData.user_data?.avatar && userProjectsData.user_data?.avatar !== "" ? ( {userProjectsData.user_data.display_name} ) : (
- {userProjectsData.user_data.first_name?.[0]} + {userProjectsData.user_data?.first_name?.[0]}
)}
@@ -118,9 +119,9 @@ export const ProfileSidebar = observer(() => {

- {userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name} + {userProjectsData.user_data?.first_name} {userProjectsData.user_data?.last_name}

-
({userProjectsData.user_data.display_name})
+
({userProjectsData.user_data?.display_name})
{userDetails.map((detail) => ( diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 43c2ce2a8b3..55b1b3c9a45 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -44,7 +44,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const handleRemove = async () => { if (!workspaceSlug || !projectId || !userDetails) return; - if (userDetails.member.id === currentUser?.id) { + if (userDetails.member?.id === currentUser?.id) { await leaveProject(workspaceSlug.toString(), projectId.toString()) .then(async () => { captureEvent(PROJECT_MEMBER_LEAVE, { @@ -62,7 +62,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { }) ); } else - await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch( + await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id).catch( (err) => setToast({ type: TOAST_TYPE.ERROR, @@ -84,12 +84,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => { />
- {userDetails.member.avatar && userDetails.member.avatar !== "" ? ( - + {userDetails.member?.avatar && userDetails.member?.avatar !== "" ? ( + {userDetails.member.display_name @@ -97,23 +97,23 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ) : ( - {(userDetails.member.display_name ?? userDetails.member.email ?? "?")[0]} + {(userDetails.member?.display_name ?? userDetails.member?.email ?? "?")[0]} )}
- + - {userDetails.member.first_name} {userDetails.member.last_name} + {userDetails.member?.first_name} {userDetails.member?.last_name}
-

{userDetails.member.display_name}

+

{userDetails.member?.display_name}

{isAdmin && ( <> -

{userDetails.member.email}

+

{userDetails.member?.email}

)}
@@ -126,12 +126,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => {
{ROLE[userDetails.role]} - {userDetails.member.id !== currentUser?.id && ( + {userDetails.member?.id !== currentUser?.id && ( @@ -142,7 +142,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { onChange={(value: EUserProjectRoles) => { if (!workspaceSlug || !projectId) return; - updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member.id, { + updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id, { role: value, }).catch((err) => { const error = err.error; @@ -156,7 +156,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { }); }} disabled={ - userDetails.member.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role + userDetails.member?.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role } placement="bottom-end" > @@ -170,8 +170,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ); })} - {(isAdmin || userDetails.member.id === currentUser?.id) && ( - + {(isAdmin || userDetails.member?.id === currentUser?.id) && ( +
diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 28581aed469..82f9685b15d 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -202,8 +202,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { {resendTimerCode > 0 ? `Request new code in ${resendTimerCode}s` : isRequestingNewCode - ? "Requesting new code" - : "Request new code"} + ? "Requesting new code" + : "Request new code"}
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 7a7c5237753..a48ea3c03dd 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -160,8 +160,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { (cycleId ? cycleDetails?.created_at : moduleId - ? moduleDetails?.created_at - : projectDetails?.created_at) ?? "" + ? moduleDetails?.created_at + : projectDetails?.created_at) ?? "" )}
)} diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index 9fc16081593..2bea1da0c1a 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -170,8 +170,8 @@ export const CreateApiTokenForm: React.FC = (props) => { {value === "custom" ? "Custom date" : selectedOption - ? selectedOption.label - : "Set expiration date"} + ? selectedOption.label + : "Set expiration date"}
} value={value} @@ -207,8 +207,8 @@ export const CreateApiTokenForm: React.FC = (props) => { ? `Expires ${renderFormattedDate(customDate)}` : null : watch("expired_at") - ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` - : null} + ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` + : null} )}
diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index ecb4aec528d..4bee1af337d 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -172,8 +172,8 @@ export const GptAssistantPopover: React.FC = (props) => { const generateResponseButtonText = isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"; + ? "Generate response" + : "Generate again"; return ( diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 2eecb1ae901..da97f2d9d17 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -78,8 +78,8 @@ export const CyclesBoardCard: FC = observer((props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycleDetails.completed_issues - ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleCopyText = (e: MouseEvent) => { diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 06db83e0d60..adf9861237a 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -216,8 +216,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 - ? "0 Issue" - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? "0 Issue" + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index 6857f7ef369..1f093986cd5 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -79,14 +79,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 3be83e31904..bc2dfb77d7f 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -312,8 +312,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { ? "Updating Estimate..." : "Update Estimate" : isSubmitting - ? "Creating Estimate..." - : "Create Estimate"} + ? "Creating Estimate..." + : "Create Estimate"}
diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 46890011041..5ef1ebf2c05 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -205,9 +205,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => ( - - ))} + {currentProjectCycleIds?.map((cycleId) => )} } /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 2a6268b5270..10717ecc39c 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -206,9 +206,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => ( - - ))} + {projectModuleIds?.map((moduleId) => )} } /> diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index a36b8cc479d..8d2b56d2ade 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -31,13 +31,7 @@ interface IBaseCalendarRoot { } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { - QuickActions, - storeType, - addIssuesToView, - viewId, - isCompletedCycle = false, - } = props; + const { QuickActions, storeType, addIssuesToView, viewId, isCompletedCycle = false } = props; // router const router = useRouter(); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 823866d9864..efd785d3e54 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -5,7 +5,15 @@ import { observer } from "mobx-react-lite"; import { Spinner } from "@plane/ui"; import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TGroupedIssues, TIssue, TIssueKanbanFilters, TIssueMap } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TGroupedIssues, + TIssue, + TIssueKanbanFilters, + TIssueMap, +} from "@plane/types"; import { ICalendarWeek } from "./types"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index d483ebe9164..3050bba7269 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -9,7 +9,13 @@ import { Popover, Transition } from "@headlessui/react"; import { Check, ChevronUp } from "lucide-react"; import { ToggleSwitch } from "@plane/ui"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TCalendarLayouts, TIssueKanbanFilters } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TCalendarLayouts, + TIssueKanbanFilters, +} from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; import { EIssueFilterType } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index f6080630f94..b112b8c3c79 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -12,9 +12,9 @@ import { useIssues } from "hooks/store"; export const ModuleCalendarLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query ; + const { workspaceSlug, projectId, moduleId } = router.query; - const {issues} = useIssues(EIssuesStoreType.MODULE) + const { issues } = useIssues(EIssuesStoreType.MODULE); if (!moduleId) return null; diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 90fcca14547..d8a2cd1a1c9 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -5,4 +5,4 @@ import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; -export const GanttLayout: React.FC = observer(() =>( )); +export const GanttLayout: React.FC = observer(() => ); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 0a492b5f717..e90823c5bf0 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -198,13 +198,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; if (kanbanFilters.includes(value)) kanbanFilters = kanbanFilters.filter((_value) => _value != value); else kanbanFilters.push(value); - updateFilters( - projectId.toString(), - EIssueFilterType.KANBAN_FILTERS, - { - [toggle]: kanbanFilters, - } - ); + updateFilters(projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { + [toggle]: kanbanFilters, + }); } }; diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 49103bcd1e6..c36fcc960ef 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -11,8 +11,8 @@ import { BaseKanBanRoot } from "../base-kanban-root"; export const ProfileIssuesKanBanLayout: React.FC = observer(() => { const { - membership: { currentWorkspaceAllProjectsRole }, -} = useUser(); + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 5777f4e70f5..ae198f1ae48 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -5,7 +5,7 @@ import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; -import { TIssue } from "@plane/types" +import { TIssue } from "@plane/types"; // components import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index a57c60d6f97..090f0ce5671 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -224,8 +224,8 @@ export const IssuePropertyLabels: React.FC = observer((pro disabled ? "cursor-not-allowed text-custom-text-200" : value.length <= maxRender - ? "cursor-pointer" - : "cursor-pointer hover:bg-custom-background-80" + ? "cursor-pointer" + : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} onClick={handleOnClick} > diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index fa89b77eda9..653cc28f29b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -57,13 +57,13 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { - if ( !projectId) return; + if (!projectId) return; updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter, }); }, - [ projectId, updateFilters] + [projectId, updateFilters] ); const renderQuickActions = useCallback( diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 67d3c904d35..dc1f4219863 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -770,4 +770,3 @@ export const IssueFormRoot: FC = observer((props) => { ); }); - diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 8023657da4a..4873e009cf1 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -159,8 +159,8 @@ export const ModuleCardItem: React.FC = observer((props) => { ? !moduleTotalIssues || moduleTotalIssues === 0 ? "0 Issue" : moduleTotalIssues === moduleDetails.completed_issues - ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` - : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` + ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` + : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` : "0 Issue"; return ( diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 03f75ca63b3..0e4904a7e76 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -176,12 +176,12 @@ export const NotificationCard: React.FC = (props) => { {notificationField === "comment" ? "commented" : notificationField === "archived_at" - ? notification.data.issue_activity.new_value === "restore" - ? "restored the issue" - : "archived the issue" - : notificationField === "None" - ? null - : replaceUnderscoreIfSnakeCase(notificationField)}{" "} + ? notification.data.issue_activity.new_value === "restore" + ? "restored the issue" + : "archived the issue" + : notificationField === "None" + ? null + : replaceUnderscoreIfSnakeCase(notificationField)}{" "} {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""} {" "} diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index 54e03b04708..c7e7a94a0b9 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -25,8 +25,8 @@ export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( {group.state_group === "unstarted" ? "Not started" : group.state_group === "started" - ? "Working on" - : STATE_GROUPS[group.state_group].label} + ? "Working on" + : STATE_GROUPS[group.state_group].label}

{group.state_count}

diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index adb9c63d721..4cab1a9f1bf 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -164,8 +164,8 @@ export const ProfileSidebar = observer(() => { completedIssuePercentage <= 35 ? "bg-red-500/10 text-red-500" : completedIssuePercentage <= 70 - ? "bg-yellow-500/10 text-yellow-500" - : "bg-green-500/10 text-green-500" + ? "bg-yellow-500/10 text-yellow-500" + : "bg-green-500/10 text-green-500" }`} > {completedIssuePercentage}% diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index cce96b9b721..24fc3652137 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -142,9 +142,8 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { if (!memberDetails?.member) return; return { value: `${memberDetails?.member.id}`, - query: `${memberDetails?.member.first_name} ${ - memberDetails?.member.last_name - } ${memberDetails?.member.display_name.toLowerCase()}`, + query: `${memberDetails?.member.first_name} ${memberDetails?.member + .last_name} ${memberDetails?.member.display_name.toLowerCase()}`, content: (
diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 8633d1586e4..d66702ccc7a 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -108,12 +108,12 @@ export const MultiLevelDropdown: React.FC = ({ height === "sm" ? "max-h-28" : height === "md" - ? "max-h-44" - : height === "rg" - ? "max-h-56" - : height === "lg" - ? "max-h-80" - : "" + ? "max-h-44" + : height === "rg" + ? "max-h-56" + : height === "lg" + ? "max-h-80" + : "" }`} > {option.children ? ( diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index c8c2e7746e7..0c3b8da0745 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -49,10 +49,10 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { type === "pending" ? "?state_group=backlog,unstarted,started" : type === "upcoming" - ? `?target_date=${today};after` - : type === "overdue" - ? `?target_date=${today};before` - : "?state_group=completed"; + ? `?target_date=${today};after` + : type === "overdue" + ? `?target_date=${today};before` + : "?state_group=completed"; return filterParams; }; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 1fb746f517f..513f9b6c4e1 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -41,13 +41,16 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce((acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, {} as { [key: string]: any[] }); + const groupedReactions = reactions.reduce( + (acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, + {} as { [key: string]: any[] } + ); return groupedReactions; }; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 0070ed20195..3e66891517a 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -172,19 +172,15 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => target_date: block.target_date ? new Date(block.target_date) : null, })); +export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { + const changedFields: Partial = {}; - export function getChangedIssuefields( - formData: Partial, - dirtyFields: { [key: string]: boolean | undefined } - ) { - const changedFields: Partial = {}; - - const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; - for (const dirtyField of dirtyFieldKeys) { - if (!!dirtyFields[dirtyField]) { - changedFields[dirtyField] = formData[dirtyField]; - } + const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; + for (const dirtyField of dirtyFieldKeys) { + if (!!dirtyFields[dirtyField]) { + changedFields[dirtyField] = formData[dirtyField]; } + } - return changedFields; - } \ No newline at end of file + return changedFields; +} diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index ed594c9f2b9..67f545a2dc9 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -21,9 +21,7 @@ export const ProfileSettingsLayout: FC = (props) => {
{header} -
- {children} -
+
{children}
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index 6e74de061d1..0b1b238a90d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -69,9 +69,10 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { title: "Success", message: issue && - `${getProjectById(issue.project_id)?.identifier}-${ - issue?.sequence_id - } is restored successfully under the project ${getProjectById(issue.project_id)?.name}`, + `${getProjectById(issue.project_id) + ?.identifier}-${issue?.sequence_id} is restored successfully under the project ${getProjectById( + issue.project_id + )?.name}`, }); router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 66466a41075..b1472a9d267 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -127,9 +127,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) return false; - const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${ - memberDetails.member?.display_name - } ${memberDetails.member.email ?? ""}`; + const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${memberDetails + .member?.display_name} ${memberDetails.member.email ?? ""}`; return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); }); return searchedWorkspaceMemberIds; From b535d8a23c8ccd3b75996754574be6d8ac777ad8 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 7 Mar 2024 13:25:23 +0530 Subject: [PATCH 038/214] [WED-634] fix: profile issues not rendering correctly with GroupBy display filter (#3886) * fix: Handled issue render in the display filters groupBy stateGroup and labels * chore: Optimized state_group filter and typo * Fix: removed workspaceLevel boolean and handled the states from stateMap --- .../issues/issue-layouts/list/default.tsx | 5 +++++ web/components/issues/issue-layouts/utils.tsx | 16 +++++++++------- web/store/issue/helpers/issue-helper.store.ts | 12 ++++++------ web/store/issue/root.store.ts | 4 ++++ web/store/state.store.ts | 18 ++++++++++++++---- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index b536da0d35d..12179ee97bb 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -77,6 +77,7 @@ const GroupByList: React.FC = (props) => { label, projectState, member, + true, true ); @@ -97,6 +98,10 @@ const GroupByList: React.FC = (props) => { preloadedData = { ...preloadedData, label_ids: [value] }; } else if (groupByKey === "assignees" && value != "None") { preloadedData = { ...preloadedData, assignee_ids: [value] }; + } else if (groupByKey === "cycle" && value != "None") { + preloadedData = { ...preloadedData, cycle_id: value }; + } else if (groupByKey === "module" && value != "None") { + preloadedData = { ...preloadedData, module_ids: [value] }; } else if (groupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 3a459ba7ae3..ffe979a56a6 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -24,7 +24,8 @@ export const getGroupByColumns = ( label: ILabelStore, projectState: IStateStore, member: IMemberRootStore, - includeNone?: boolean + includeNone?: boolean, + isWorkspaceLevel?: boolean ): IGroupByColumn[] | undefined => { switch (groupBy) { case "project": @@ -40,7 +41,7 @@ export const getGroupByColumns = ( case "priority": return getPriorityColumns(); case "labels": - return getLabelsColumns(label) as any; + return getLabelsColumns(label, isWorkspaceLevel) as any; case "assignees": return getAssigneeColumns(member) as any; case "created_by": @@ -177,12 +178,13 @@ const getPriorityColumns = () => { })); }; -const getLabelsColumns = (label: ILabelStore) => { - const { projectLabels } = label; +const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) => { + const { workspaceLabels, projectLabels } = label; - if (!projectLabels) return; - - const labels = [...projectLabels, { id: "None", name: "None", color: "#666" }]; + const labels = [ + ...(isWorkspaceLevel ? workspaceLabels || [] : projectLabels || []), + { id: "None", name: "None", color: "#666" }, + ]; return labels.map((label) => ({ id: label.id, diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 235f65e7c71..50e04e890b2 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -76,8 +76,8 @@ export class IssueHelperStore implements TIssueHelperStore { 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 || {})?.[_issue?.state_id]?.group || "None"; groupArray = [state_group]; } else { const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); @@ -117,8 +117,8 @@ export class IssueHelperStore implements TIssueHelperStore { 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 || {})?.[_issue?.state_id]?.group || "None"; + subGroupArray = [state_group]; groupArray = [state_group]; } else { @@ -233,10 +233,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]; diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index a9dde82ae3e..68206a70464 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -35,6 +35,7 @@ export interface IIssueRootStore { userId: string | undefined; // user profile detail Id stateMap: Record | undefined; stateDetails: IState[] | undefined; + workspaceStateDetails: IState[] | undefined; labelMap: Record | undefined; workSpaceMemberRolesMap: Record | undefined; memberMap: Record | undefined; @@ -89,6 +90,7 @@ export class IssueRootStore implements IIssueRootStore { userId: string | undefined = undefined; stateMap: Record | undefined = undefined; stateDetails: IState[] | undefined = undefined; + workspaceStateDetails: IState[] | undefined = undefined; labelMap: Record | undefined = undefined; workSpaceMemberRolesMap: Record | undefined = undefined; memberMap: Record | undefined = undefined; @@ -142,6 +144,7 @@ export class IssueRootStore implements IIssueRootStore { globalViewId: observable.ref, stateMap: observable, stateDetails: observable, + workspaceStateDetails: observable, labelMap: observable, memberMap: observable, workSpaceMemberRolesMap: observable, @@ -163,6 +166,7 @@ export class IssueRootStore implements IIssueRootStore { if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId; if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap; if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates; + if (!isEmpty(rootStore?.state?.workspaceStates)) this.workspaceStateDetails = rootStore?.state?.workspaceStates; if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap; if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap)) this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined; diff --git a/web/store/state.store.ts b/web/store/state.store.ts index df3496f394c..eaece6db0dc 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -17,6 +17,7 @@ export interface IStateStore { // observables stateMap: Record; // computed + workspaceStates: IState[] | undefined; projectStates: IState[] | undefined; groupedProjectStates: Record | undefined; // computed actions @@ -73,13 +74,22 @@ export class StateStore implements IStateStore { this.router = _rootStore.app.router; } + /** + * Returns the stateMap belongs to a specific workspace + */ + get workspaceStates() { + const workspaceSlug = this.router.workspaceSlug || ""; + if (!workspaceSlug || !this.fetchedMap[workspaceSlug]) return; + return sortStates(Object.values(this.stateMap)); + } + /** * Returns the stateMap belongs to a specific project */ get projectStates() { const projectId = this.router.projectId; - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); } @@ -106,8 +116,8 @@ export class StateStore implements IStateStore { * @returns IState[] */ getProjectStates = computedFn((projectId: string) => { - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); }); From b03f6a81e20810f9f658f0052f38fd394510a9e0 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:26:58 +0530 Subject: [PATCH 039/214] fix: project members settings flickering (#3894) --- web/components/project/member-select.tsx | 4 ++-- .../project/project-settings-member-defaults.tsx | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 2aa83cd0604..e6e0335e622 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -49,14 +49,14 @@ export const MemberSelect: React.FC = observer((props) => { +
{selectedOption && } {selectedOption ? ( selectedOption.member?.display_name ) : (
- None + None
)}
diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx index d6cffa7a44e..89695e8911c 100644 --- a/web/components/project/project-settings-member-defaults.tsx +++ b/web/components/project/project-settings-member-defaults.tsx @@ -63,11 +63,14 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => { }); await updateProject(workspaceSlug.toString(), projectId.toString(), { - default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee, - project_lead: formData.project_lead === "none" ? null : formData.project_lead, + default_assignee: + formData.default_assignee === "none" + ? null + : formData.default_assignee ?? currentProjectDetails?.default_assignee, + project_lead: + formData.project_lead === "none" ? null : formData.project_lead ?? currentProjectDetails?.project_lead, }) .then(() => { - fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); setToast({ title: "Success", type: TOAST_TYPE.SUCCESS, From bc02e56e3ce171fff9f97a8faa88ac877f79c668 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:33:12 +0530 Subject: [PATCH 040/214] [WEB-664] refactor: folder structure (#3884) * refactor: folder structure * chore: resolved merge conflicts --- apiserver/plane/app/urls/issue.py | 12 +- apiserver/plane/app/views/__init__.py | 165 +- .../views/{analytic.py => analytic/base.py} | 0 .../app/views/{asset.py => asset/base.py} | 2 +- .../app/views/{cycle.py => cycle/base.py} | 274 +- apiserver/plane/app/views/cycle/issue.py | 312 +++ .../views/{dashboard.py => dashboard/base.py} | 2 +- .../views/{estimate.py => estimate/base.py} | 2 +- .../views/{exporter.py => exporter/base.py} | 2 +- .../views/{external.py => external/base.py} | 2 +- .../app/views/{inbox.py => inbox/base.py} | 2 +- apiserver/plane/app/views/issue.py | 2462 ----------------- apiserver/plane/app/views/issue/activity.py | 85 + apiserver/plane/app/views/issue/archive.py | 347 +++ apiserver/plane/app/views/issue/attachment.py | 73 + apiserver/plane/app/views/issue/base.py | 686 +++++ apiserver/plane/app/views/issue/comment.py | 219 ++ apiserver/plane/app/views/issue/draft.py | 367 +++ apiserver/plane/app/views/issue/label.py | 105 + apiserver/plane/app/views/issue/link.py | 120 + apiserver/plane/app/views/issue/reaction.py | 89 + apiserver/plane/app/views/issue/relation.py | 204 ++ apiserver/plane/app/views/issue/sub_issue.py | 195 ++ apiserver/plane/app/views/issue/subscriber.py | 124 + .../app/views/{module.py => module/base.py} | 237 +- apiserver/plane/app/views/module/issue.py | 259 ++ .../{notification.py => notification/base.py} | 2 +- .../plane/app/views/{page.py => page/base.py} | 2 +- apiserver/plane/app/views/project.py | 1146 -------- apiserver/plane/app/views/project/base.py | 549 ++++ apiserver/plane/app/views/project/invite.py | 286 ++ apiserver/plane/app/views/project/member.py | 349 +++ .../app/views/{state.py => state/base.py} | 2 +- .../plane/app/views/{user.py => user/base.py} | 0 .../plane/app/views/{view.py => view/base.py} | 2 +- .../app/views/{webhook.py => webhook/base.py} | 2 +- apiserver/plane/app/views/workspace.py | 1843 ------------ apiserver/plane/app/views/workspace/base.py | 414 +++ apiserver/plane/app/views/workspace/cycle.py | 116 + .../plane/app/views/workspace/estimate.py | 39 + apiserver/plane/app/views/workspace/invite.py | 301 ++ apiserver/plane/app/views/workspace/label.py | 25 + apiserver/plane/app/views/workspace/member.py | 396 +++ apiserver/plane/app/views/workspace/module.py | 104 + apiserver/plane/app/views/workspace/state.py | 25 + apiserver/plane/app/views/workspace/user.py | 573 ++++ 46 files changed, 6492 insertions(+), 6031 deletions(-) rename apiserver/plane/app/views/{analytic.py => analytic/base.py} (100%) rename apiserver/plane/app/views/{asset.py => asset/base.py} (98%) rename apiserver/plane/app/views/{cycle.py => cycle/base.py} (78%) create mode 100644 apiserver/plane/app/views/cycle/issue.py rename apiserver/plane/app/views/{dashboard.py => dashboard/base.py} (99%) rename apiserver/plane/app/views/{estimate.py => estimate/base.py} (99%) rename apiserver/plane/app/views/{exporter.py => exporter/base.py} (99%) rename apiserver/plane/app/views/{external.py => external/base.py} (99%) rename apiserver/plane/app/views/{inbox.py => inbox/base.py} (99%) delete mode 100644 apiserver/plane/app/views/issue.py create mode 100644 apiserver/plane/app/views/issue/activity.py create mode 100644 apiserver/plane/app/views/issue/archive.py create mode 100644 apiserver/plane/app/views/issue/attachment.py create mode 100644 apiserver/plane/app/views/issue/base.py create mode 100644 apiserver/plane/app/views/issue/comment.py create mode 100644 apiserver/plane/app/views/issue/draft.py create mode 100644 apiserver/plane/app/views/issue/label.py create mode 100644 apiserver/plane/app/views/issue/link.py create mode 100644 apiserver/plane/app/views/issue/reaction.py create mode 100644 apiserver/plane/app/views/issue/relation.py create mode 100644 apiserver/plane/app/views/issue/sub_issue.py create mode 100644 apiserver/plane/app/views/issue/subscriber.py rename apiserver/plane/app/views/{module.py => module/base.py} (67%) create mode 100644 apiserver/plane/app/views/module/issue.py rename apiserver/plane/app/views/{notification.py => notification/base.py} (99%) rename apiserver/plane/app/views/{page.py => page/base.py} (99%) delete mode 100644 apiserver/plane/app/views/project.py create mode 100644 apiserver/plane/app/views/project/base.py create mode 100644 apiserver/plane/app/views/project/invite.py create mode 100644 apiserver/plane/app/views/project/member.py rename apiserver/plane/app/views/{state.py => state/base.py} (99%) rename apiserver/plane/app/views/{user.py => user/base.py} (100%) rename apiserver/plane/app/views/{view.py => view/base.py} (99%) rename apiserver/plane/app/views/{webhook.py => webhook/base.py} (99%) delete mode 100644 apiserver/plane/app/views/workspace.py create mode 100644 apiserver/plane/app/views/workspace/base.py create mode 100644 apiserver/plane/app/views/workspace/cycle.py create mode 100644 apiserver/plane/app/views/workspace/estimate.py create mode 100644 apiserver/plane/app/views/workspace/invite.py create mode 100644 apiserver/plane/app/views/workspace/label.py create mode 100644 apiserver/plane/app/views/workspace/member.py create mode 100644 apiserver/plane/app/views/workspace/module.py create mode 100644 apiserver/plane/app/views/workspace/state.py create mode 100644 apiserver/plane/app/views/workspace/user.py diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 6b677287b91..0d3b9e0634c 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -3,14 +3,15 @@ from plane.app.views import ( BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, + SubIssuesEndpoint, + IssueLinkViewSet, + IssueAttachmentEndpoint, CommentReactionViewSet, ExportIssuesEndpoint, IssueActivityEndpoint, IssueArchiveViewSet, - IssueAttachmentEndpoint, IssueCommentViewSet, IssueDraftViewSet, - IssueLinkViewSet, IssueListEndpoint, IssueReactionViewSet, IssueRelationViewSet, @@ -18,8 +19,6 @@ IssueUserDisplayPropertyEndpoint, IssueViewSet, LabelViewSet, - SubIssuesEndpoint, - UserWorkSpaceIssues, ) urlpatterns = [ @@ -82,11 +81,6 @@ BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), - path( - "workspaces//my-issues/", - UserWorkSpaceIssues.as_view(), - name="workspace-issues", - ), ## path( "workspaces//projects//issues//sub-issues/", diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index dd668bd6e75..bb5b7dd74d9 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -1,19 +1,26 @@ -from .project import ( +from .project.base import ( ProjectViewSet, - ProjectMemberViewSet, - UserProjectInvitationsViewset, - ProjectInvitationsViewset, - AddTeamToProjectEndpoint, ProjectIdentifierEndpoint, - ProjectJoinEndpoint, ProjectUserViewsEndpoint, - ProjectMemberUserEndpoint, ProjectFavoritesViewSet, ProjectPublicCoverImagesEndpoint, ProjectDeployBoardViewSet, +) + +from .project.invite import ( + UserProjectInvitationsViewset, + ProjectInvitationsViewset, + ProjectJoinEndpoint, +) + +from .project.member import ( + ProjectMemberViewSet, + AddTeamToProjectEndpoint, + ProjectMemberUserEndpoint, UserProjectRolesEndpoint, ) -from .user import ( + +from .user.base import ( UserEndpoint, UpdateUserOnBoardedEndpoint, UpdateUserTourCompletedEndpoint, @@ -24,71 +31,121 @@ from .base import BaseAPIView, BaseViewSet, WebhookMixin -from .workspace import ( +from .workspace.base import ( WorkSpaceViewSet, UserWorkSpacesEndpoint, WorkSpaceAvailabilityCheckEndpoint, - WorkspaceJoinEndpoint, + UserWorkspaceDashboardEndpoint, + WorkspaceThemeViewSet, + ExportWorkspaceUserActivityEndpoint +) + +from .workspace.member import ( WorkSpaceMemberViewSet, TeamMemberViewSet, + WorkspaceMemberUserEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceMemberUserViewsEndpoint, +) +from .workspace.invite import ( WorkspaceInvitationsViewset, + WorkspaceJoinEndpoint, UserWorkspaceInvitationsViewSet, +) +from .workspace.label import ( + WorkspaceLabelsEndpoint, +) +from .workspace.state import ( + WorkspaceStatesEndpoint, +) +from .workspace.user import ( UserLastProjectWithWorkspaceEndpoint, - WorkspaceMemberUserEndpoint, - WorkspaceMemberUserViewsEndpoint, - UserActivityGraphEndpoint, - UserIssueCompletedGraphEndpoint, - UserWorkspaceDashboardEndpoint, - WorkspaceThemeViewSet, - WorkspaceUserProfileStatsEndpoint, - WorkspaceUserActivityEndpoint, - WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, - WorkspaceLabelsEndpoint, - WorkspaceProjectMemberEndpoint, WorkspaceUserPropertiesEndpoint, - WorkspaceStatesEndpoint, + WorkspaceUserProfileEndpoint, + WorkspaceUserActivityEndpoint, + WorkspaceUserProfileStatsEndpoint, + UserActivityGraphEndpoint, + UserIssueCompletedGraphEndpoint, +) +from .workspace.estimate import ( WorkspaceEstimatesEndpoint, - ExportWorkspaceUserActivityEndpoint, +) +from .workspace.module import ( WorkspaceModulesEndpoint, +) +from .workspace.cycle import ( WorkspaceCyclesEndpoint, ) -from .state import StateViewSet -from .view import ( + +from .state.base import StateViewSet +from .view.base import ( GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet, ) -from .cycle import ( +from .cycle.base import ( CycleViewSet, - CycleIssueViewSet, CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, ) -from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet -from .issue import ( +from .cycle.issue import ( + CycleIssueViewSet, +) + +from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet +from .issue.base import ( IssueListEndpoint, IssueViewSet, - WorkSpaceIssuesEndpoint, - IssueActivityEndpoint, - IssueCommentViewSet, IssueUserDisplayPropertyEndpoint, - LabelViewSet, BulkDeleteIssuesEndpoint, - UserWorkSpaceIssues, - SubIssuesEndpoint, - IssueLinkViewSet, - BulkCreateIssueLabelsEndpoint, - IssueAttachmentEndpoint, +) + +from .issue.activity import ( + IssueActivityEndpoint, +) + +from .issue.archive import ( IssueArchiveViewSet, - IssueSubscriberViewSet, +) + +from .issue.attachment import ( + IssueAttachmentEndpoint, +) + +from .issue.comment import ( + IssueCommentViewSet, CommentReactionViewSet, - IssueReactionViewSet, +) + +from .issue.draft import IssueDraftViewSet + +from .issue.label import ( + LabelViewSet, + BulkCreateIssueLabelsEndpoint, +) + +from .issue.link import ( + IssueLinkViewSet, +) + +from .issue.relation import ( IssueRelationViewSet, - IssueDraftViewSet, +) + +from .issue.reaction import ( + IssueReactionViewSet, +) + +from .issue.sub_issue import ( + SubIssuesEndpoint, +) + +from .issue.subscriber import ( + IssueSubscriberViewSet, ) from .auth_extended import ( @@ -107,17 +164,21 @@ MagicSignInEndpoint, ) -from .module import ( +from .module.base import ( ModuleViewSet, - ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, ModuleUserPropertiesEndpoint, ) +from .module.issue import ( + ModuleIssueViewSet, +) + from .api import ApiTokenEndpoint -from .page import ( + +from .page.base import ( PageViewSet, PageFavoriteViewSet, PageLogEndpoint, @@ -127,19 +188,19 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint -from .external import ( +from .external.base import ( GPTIntegrationEndpoint, UnsplashEndpoint, ) -from .estimate import ( +from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) -from .inbox import InboxViewSet, InboxIssueViewSet +from .inbox.base import InboxViewSet, InboxIssueViewSet -from .analytic import ( +from .analytic.base import ( AnalyticsEndpoint, AnalyticViewViewset, SavedAnalyticEndpoint, @@ -147,23 +208,23 @@ DefaultAnalyticsEndpoint, ) -from .notification import ( +from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet, UserNotificationPreferenceEndpoint, ) -from .exporter import ExportIssuesEndpoint +from .exporter.base import ExportIssuesEndpoint from .config import ConfigurationEndpoint, MobileConfigurationEndpoint -from .webhook import ( +from .webhook.base import ( WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint, ) -from .dashboard import DashboardEndpoint, WidgetsEndpoint +from .dashboard.base import DashboardEndpoint, WidgetsEndpoint from .error_404 import custom_404_view diff --git a/apiserver/plane/app/views/analytic.py b/apiserver/plane/app/views/analytic/base.py similarity index 100% rename from apiserver/plane/app/views/analytic.py rename to apiserver/plane/app/views/analytic/base.py diff --git a/apiserver/plane/app/views/asset.py b/apiserver/plane/app/views/asset/base.py similarity index 98% rename from apiserver/plane/app/views/asset.py rename to apiserver/plane/app/views/asset/base.py index fb559061011..6de4a4ee7f0 100644 --- a/apiserver/plane/app/views/asset.py +++ b/apiserver/plane/app/views/asset/base.py @@ -4,7 +4,7 @@ from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet from plane.db.models import FileAsset, Workspace from plane.app.serializers import FileAssetSerializer diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle/base.py similarity index 78% rename from apiserver/plane/app/views/cycle.py rename to apiserver/plane/app/views/cycle/base.py index 586da053b8e..9dc25474fff 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -29,7 +29,7 @@ from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin +from .. import BaseViewSet, BaseAPIView, WebhookMixin from plane.app.serializers import ( CycleSerializer, CycleIssueSerializer, @@ -660,278 +660,6 @@ def destroy(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class CycleIssueViewSet(WebhookMixin, BaseViewSet): - serializer_class = CycleIssueSerializer - model = CycleIssue - - webhook_event = "cycle_issue" - bulk = True - - permission_classes = [ - ProjectEntityPermission, - ] - - filterset_fields = [ - "issue__labels__id", - "issue__assignees__id", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("issue_id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .filter(cycle_id=self.kwargs.get("cycle_id")) - .select_related("project") - .select_related("workspace") - .select_related("cycle") - .select_related("issue", "issue__state", "issue__project") - .prefetch_related("issue__assignees", "issue__labels") - .distinct() - ) - - @method_decorator(gzip_page) - def list(self, request, slug, project_id, cycle_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - order_by = request.GET.get("order_by", "created_at") - filters = issue_filters(request.query_params, "GET") - queryset = ( - Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .filter(**filters) - .select_related("workspace", "project", "state", "parent") - .prefetch_related( - "assignees", - "labels", - "issue_module__module", - "issue_cycle__cycle", - ) - .order_by(order_by) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .order_by(order_by) - ) - if self.fields: - issues = IssueSerializer( - queryset, many=True, fields=fields if fields else None - ).data - else: - issues = queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id, cycle_id): - issues = request.data.get("issues", []) - - if not issues: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - cycle = Cycle.objects.get( - workspace__slug=slug, project_id=project_id, pk=cycle_id - ) - - if ( - cycle.end_date is not None - and cycle.end_date < timezone.now().date() - ): - return Response( - { - "error": "The Cycle has already been completed so no new issues can be added" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get all CycleIssues already created - cycle_issues = list( - CycleIssue.objects.filter( - ~Q(cycle_id=cycle_id), issue_id__in=issues - ) - ) - existing_issues = [ - str(cycle_issue.issue_id) for cycle_issue in cycle_issues - ] - new_issues = list(set(issues) - set(existing_issues)) - - # New issues to create - created_records = CycleIssue.objects.bulk_create( - [ - CycleIssue( - project_id=project_id, - workspace_id=cycle.workspace_id, - created_by_id=request.user.id, - updated_by_id=request.user.id, - cycle_id=cycle_id, - issue_id=issue, - ) - for issue in new_issues - ], - batch_size=10, - ) - - # Updated Issues - updated_records = [] - update_cycle_issue_activity = [] - # Iterate over each cycle_issue in cycle_issues - for cycle_issue in cycle_issues: - # Update the cycle_issue's cycle_id - cycle_issue.cycle_id = cycle_id - # Add the modified cycle_issue to the records_to_update list - updated_records.append(cycle_issue) - # Record the update activity - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_issue.cycle_id), - "new_cycle_id": str(cycle_id), - "issue_id": str(cycle_issue.issue_id), - } - ) - - # Update the cycle issues - CycleIssue.objects.bulk_update( - updated_records, ["cycle_id"], batch_size=100 - ) - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": issues}), - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": serializers.serialize( - "json", created_records - ), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - def destroy(self, request, slug, project_id, cycle_id, issue_id): - cycle_issue = CycleIssue.objects.get( - issue_id=issue_id, - workspace__slug=slug, - project_id=project_id, - cycle_id=cycle_id, - ) - issue_activity.delay( - type="cycle.activity.deleted", - requested_data=json.dumps( - { - "cycle_id": str(self.kwargs.get("cycle_id")), - "issues": [str(issue_id)], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(issue_id), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - cycle_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - class CycleDateCheckEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py new file mode 100644 index 00000000000..84af4ff324d --- /dev/null +++ b/apiserver/plane/app/views/cycle/issue.py @@ -0,0 +1,312 @@ +# Python imports +import json + +# Django imports +from django.db.models import ( + Func, + F, + Q, + OuterRef, + Value, + UUIDField, +) +from django.core import serializers +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueSerializer, + CycleIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Cycle, + CycleIssue, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class CycleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = CycleIssueSerializer + model = CycleIssue + + webhook_event = "cycle_issue" + bulk = True + + permission_classes = [ + ProjectEntityPermission, + ] + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue_id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .filter(cycle_id=self.kwargs.get("cycle_id")) + .select_related("project") + .select_related("workspace") + .select_related("cycle") + .select_related("issue", "issue__state", "issue__project") + .prefetch_related("issue__assignees", "issue__labels") + .distinct() + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, cycle_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + order_by = request.GET.get("order_by", "created_at") + filters = issue_filters(request.query_params, "GET") + queryset = ( + Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", + "labels", + "issue_module__module", + "issue_cycle__cycle", + ) + .order_by(order_by) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by(order_by) + ) + if self.fields: + issues = IssueSerializer( + queryset, many=True, fields=fields if fields else None + ).data + else: + issues = queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, cycle_id): + issues = request.data.get("issues", []) + + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) + + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get all CycleIssues already created + cycle_issues = list( + CycleIssue.objects.filter( + ~Q(cycle_id=cycle_id), issue_id__in=issues + ) + ) + existing_issues = [ + str(cycle_issue.issue_id) for cycle_issue in cycle_issues + ] + new_issues = list(set(issues) - set(existing_issues)) + + # New issues to create + created_records = CycleIssue.objects.bulk_create( + [ + CycleIssue( + project_id=project_id, + workspace_id=cycle.workspace_id, + created_by_id=request.user.id, + updated_by_id=request.user.id, + cycle_id=cycle_id, + issue_id=issue, + ) + for issue in new_issues + ], + batch_size=10, + ) + + # Updated Issues + updated_records = [] + update_cycle_issue_activity = [] + # Iterate over each cycle_issue in cycle_issues + for cycle_issue in cycle_issues: + # Update the cycle_issue's cycle_id + cycle_issue.cycle_id = cycle_id + # Add the modified cycle_issue to the records_to_update list + updated_records.append(cycle_issue) + # Record the update activity + update_cycle_issue_activity.append( + { + "old_cycle_id": str(cycle_issue.cycle_id), + "new_cycle_id": str(cycle_id), + "issue_id": str(cycle_issue.issue_id), + } + ) + + # Update the cycle issues + CycleIssue.objects.bulk_update( + updated_records, ["cycle_id"], batch_size=100 + ) + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=json.dumps({"cycles_list": issues}), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_cycle_issues": update_cycle_issue_activity, + "created_cycle_issues": serializers.serialize( + "json", created_records + ), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, cycle_id, issue_id): + cycle_issue = CycleIssue.objects.get( + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, + ) + issue_activity.delay( + type="cycle.activity.deleted", + requested_data=json.dumps( + { + "cycle_id": str(self.kwargs.get("cycle_id")), + "issues": [str(issue_id)], + } + ), + actor_id=str(self.request.user.id), + issue_id=str(issue_id), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + cycle_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard/base.py similarity index 99% rename from apiserver/plane/app/views/dashboard.py rename to apiserver/plane/app/views/dashboard/base.py index 144ae74a978..27e45f59c8c 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -26,7 +26,7 @@ from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.db.models import ( Issue, IssueActivity, diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate/base.py similarity index 99% rename from apiserver/plane/app/views/estimate.py rename to apiserver/plane/app/views/estimate/base.py index eae2e3351dc..7ac3035a956 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -3,7 +3,7 @@ from rest_framework import status # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Project, Estimate, EstimatePoint from plane.app.serializers import ( diff --git a/apiserver/plane/app/views/exporter.py b/apiserver/plane/app/views/exporter/base.py similarity index 99% rename from apiserver/plane/app/views/exporter.py rename to apiserver/plane/app/views/exporter/base.py index 4e2d0760a38..846508515ad 100644 --- a/apiserver/plane/app/views/exporter.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -3,7 +3,7 @@ from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.app.permissions import WorkSpaceAdminPermission from plane.bgtasks.export_task import issue_export_task from plane.db.models import Project, ExporterHistory, Workspace diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external/base.py similarity index 99% rename from apiserver/plane/app/views/external.py rename to apiserver/plane/app/views/external/base.py index 66667fe5675..2d5d2c7aa4b 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external/base.py @@ -10,7 +10,7 @@ # Django imports # Module imports -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Workspace, Project from plane.app.serializers import ( diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox/base.py similarity index 99% rename from apiserver/plane/app/views/inbox.py rename to apiserver/plane/app/views/inbox/base.py index dad337babb0..fb3b9227f2d 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -15,7 +15,7 @@ from rest_framework.response import Response # Module imports -from .base import BaseViewSet +from ..base import BaseViewSet from plane.app.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( Inbox, diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py deleted file mode 100644 index fc05343dece..00000000000 --- a/apiserver/plane/app/views/issue.py +++ /dev/null @@ -1,2462 +0,0 @@ -# Python imports -import json -import random -from itertools import chain - -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, -) -from django.core.serializers.json import DjangoJSONEncoder -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page -from django.db import IntegrityError -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import UUIDField -from django.db.models.functions import Coalesce - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser - -# Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - IssueActivitySerializer, - IssueCommentSerializer, - IssuePropertySerializer, - IssueSerializer, - IssueCreateSerializer, - LabelSerializer, - IssueFlatSerializer, - IssueLinkSerializer, - IssueLiteSerializer, - IssueAttachmentSerializer, - IssueSubscriberSerializer, - ProjectMemberLiteSerializer, - IssueReactionSerializer, - CommentReactionSerializer, - IssueRelationSerializer, - RelatedIssueSerializer, - IssueDetailSerializer, -) -from plane.app.permissions import ( - ProjectEntityPermission, - WorkSpaceAdminPermission, - ProjectMemberPermission, - ProjectLitePermission, -) -from plane.db.models import ( - Project, - Issue, - IssueActivity, - IssueComment, - IssueProperty, - Label, - IssueLink, - IssueAttachment, - IssueSubscriber, - ProjectMember, - IssueReaction, - CommentReaction, - IssueRelation, -) -from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters -from collections import defaultdict -from plane.utils.cache import invalidate_cache - - -class IssueListEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - issue_ids = request.GET.get("issues", False) - - if not issue_ids: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_ids = [ - issue_id for issue_id in issue_ids.split(",") if issue_id != "" - ] - - queryset = ( - Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = queryset.filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - if self.fields or self.expand: - issues = IssueSerializer( - queryset, many=True, fields=self.fields, expand=self.expand - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - -class IssueViewSet(WebhookMixin, BaseViewSet): - def get_serializer_class(self): - return ( - IssueCreateSerializer - if self.action in ["create", "update", "partial_update"] - else IssueSerializer - ) - - model = Issue - webhook_event = "issue" - permission_classes = [ - ProjectEntityPermission, - ] - - search_fields = [ - "name", - ] - - filterset_fields = [ - "state__name", - "assignees__id", - "workspace__id", - ] - - def get_queryset(self): - return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id") - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - # Only use serializer when expand or fields else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save() - - # Track the issue - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - .first() - ) - return Response(issue, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - current_instance = json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ) - - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = self.get_queryset().filter(pk=pk).first() - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -# TODO: deprecated remove once confirmed -class UserWorkSpaceIssues(BaseAPIView): - @method_decorator(gzip_page) - def get(self, request, slug): - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.filter( - ( - Q(assignees__in=[request.user]) - | Q(created_by=request.user) - | Q(issue_subscribers__subscriber=request.user) - ), - workspace__slug=slug, - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by_param) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(**filters) - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - ## Grouping the results - group_by = request.GET.get("group_by", False) - sub_group_by = request.GET.get("sub_group_by", False) - if sub_group_by and sub_group_by == group_by: - return Response( - {"error": "Group by and sub group by cannot be same"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if group_by: - grouped_results = group_results(issues, group_by, sub_group_by) - return Response( - grouped_results, - status=status.HTTP_200_OK, - ) - - return Response(issues, status=status.HTTP_200_OK) - - -# TODO: deprecated remove once confirmed -class WorkSpaceIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug): - issues = ( - Issue.issue_objects.filter(workspace__slug=slug) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - ) - serializer = IssueSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueActivityEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - filters = {} - if request.GET.get("created_at__gt", None) is not None: - filters = {"created_at__gt": request.GET.get("created_at__gt")} - - issue_activities = ( - IssueActivity.objects.filter(issue_id=issue_id) - .filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .select_related("actor", "workspace", "issue", "project") - ).order_by("created_at") - issue_comments = ( - IssueComment.objects.filter(issue_id=issue_id) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .order_by("created_at") - .select_related("actor", "issue", "project", "workspace") - .prefetch_related( - Prefetch( - "comment_reactions", - queryset=CommentReaction.objects.select_related("actor"), - ) - ) - ) - issue_activities = IssueActivitySerializer( - issue_activities, many=True - ).data - issue_comments = IssueCommentSerializer(issue_comments, many=True).data - - if request.GET.get("activity_type", None) == "issue-property": - return Response(issue_activities, status=status.HTTP_200_OK) - - if request.GET.get("activity_type", None) == "issue-comment": - return Response(issue_comments, status=status.HTTP_200_OK) - - result_list = sorted( - chain(issue_activities, issue_comments), - key=lambda instance: instance["created_at"], - ) - - return Response(result_list, status=status.HTTP_200_OK) - - -class IssueCommentViewSet(WebhookMixin, BaseViewSet): - serializer_class = IssueCommentSerializer - model = IssueComment - webhook_event = "issue_comment" - permission_classes = [ - ProjectLitePermission, - ] - - filterset_fields = [ - "issue__id", - "workspace__id", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - member_id=self.request.user.id, - is_active=True, - ) - ) - ) - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueCommentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - actor=request.user, - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueCommentSerializer( - issue_comment, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="comment.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - issue_comment.delete() - issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps({"comment_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueUserDisplayPropertyEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] - - def patch(self, request, slug, project_id): - issue_property = IssueProperty.objects.get( - user=request.user, - project_id=project_id, - ) - - issue_property.filters = request.data.get( - "filters", issue_property.filters - ) - issue_property.display_filters = request.data.get( - "display_filters", issue_property.display_filters - ) - issue_property.display_properties = request.data.get( - "display_properties", issue_property.display_properties - ) - issue_property.save() - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug, project_id): - issue_property, _ = IssueProperty.objects.get_or_create( - user=request.user, project_id=project_id - ) - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class LabelViewSet(BaseViewSet): - serializer_class = LabelSerializer - model = Label - permission_classes = [ - ProjectMemberPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) - .select_related("project") - .select_related("workspace") - .select_related("parent") - .distinct() - .order_by("sort_order") - ) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def create(self, request, slug, project_id): - try: - serializer = LabelSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - except IntegrityError: - return Response( - { - "error": "Label with the same name already exists in the project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def partial_update(self, request, *args, **kwargs): - return super().partial_update(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - -class BulkDeleteIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def delete(self, request, slug, project_id): - issue_ids = request.data.get("issue_ids", []) - - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issues = Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - - total_issues = len(issues) - - issues.delete() - - return Response( - {"message": f"{total_issues} issues were deleted"}, - status=status.HTTP_200_OK, - ) - - -class SubIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - sub_issues = ( - Issue.issue_objects.filter( - parent_id=issue_id, workspace__slug=slug - ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .annotate(state_group=F("state__group")) - ) - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - sub_issues = sub_issues.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response( - { - "sub_issues": sub_issues, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - # Assign multiple sub issues - def post(self, request, slug, project_id, issue_id): - parent_issue = Issue.issue_objects.get(pk=issue_id) - sub_issue_ids = request.data.get("sub_issue_ids", []) - - if not len(sub_issue_ids): - return Response( - {"error": "Sub Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) - - for sub_issue in sub_issues: - sub_issue.parent = parent_issue - - _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - - updated_sub_issues = Issue.issue_objects.filter( - id__in=sub_issue_ids - ).annotate(state_group=F("state__group")) - - # Track the issue - _ = [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"parent": str(issue_id)}), - actor_id=str(request.user.id), - issue_id=str(sub_issue_id), - project_id=str(project_id), - current_instance=json.dumps({"parent": str(sub_issue_id)}), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for sub_issue_id in sub_issue_ids - ] - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in updated_sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - serializer = IssueSerializer( - updated_sub_issues, - many=True, - ) - return Response( - { - "sub_issues": serializer.data, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - -class IssueLinkViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - model = IssueLink - serializer_class = IssueLinkSerializer - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueLinkSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - ) - issue_activity.delay( - type="link.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueLinkSerializer( - issue_link, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="link.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - issue_activity.delay( - type="link.activity.deleted", - requested_data=json.dumps({"link_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_link.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class BulkCreateIssueLabelsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - label_data = request.data.get("label_data", []) - project = Project.objects.get(pk=project_id) - - labels = Label.objects.bulk_create( - [ - Label( - name=label.get("name", "Migrated"), - description=label.get("description", "Migrated Issue"), - color="#" + "%06x" % random.randint(0, 0xFFFFFF), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for label in label_data - ], - batch_size=50, - ignore_conflicts=True, - ) - - return Response( - {"labels": LabelSerializer(labels, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class IssueAttachmentEndpoint(BaseAPIView): - serializer_class = IssueAttachmentSerializer - permission_classes = [ - ProjectEntityPermission, - ] - model = IssueAttachment - parser_classes = (MultiPartParser, FormParser) - - def post(self, request, slug, project_id, issue_id): - serializer = IssueAttachmentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id, issue_id=issue_id) - issue_activity.delay( - type="attachment.activity.created", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - serializer.data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, slug, project_id, issue_id, pk): - issue_attachment = IssueAttachment.objects.get(pk=pk) - issue_attachment.asset.delete(save=False) - issue_attachment.delete() - issue_activity.delay( - type="attachment.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - return Response(status=status.HTTP_204_NO_CONTENT) - - def get(self, request, slug, project_id, issue_id): - issue_attachments = IssueAttachment.objects.filter( - issue_id=issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueAttachmentSerializer(issue_attachments, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueArchiveViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(archived_at__isnull=False) - .filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ) - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - show_sub_issues = request.GET.get("show_sub_issues", "true") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issue_queryset = ( - issue_queryset - if show_sub_issues == "true" - else issue_queryset.filter(parent__isnull=True) - ) - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def archive(self, request, slug, project_id, pk=None): - issue = Issue.issue_objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - ) - if issue.state.group not in ["completed", "cancelled"]: - return Response( - { - "error": "Can only archive completed or cancelled state group issue" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps( - { - "archived_at": str(timezone.now().date()), - "automation": False, - } - ), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = timezone.now().date() - issue.save() - - return Response( - {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK - ) - - def unarchive(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": None}), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = None - issue.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueSubscriberViewSet(BaseViewSet): - serializer_class = IssueSubscriberSerializer - model = IssueSubscriber - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_permissions(self): - if self.action in ["subscribe", "unsubscribe", "subscription_status"]: - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectEntityPermission, - ] - - return super(IssueSubscriberViewSet, self).get_permissions() - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_id=self.kwargs.get("issue_id"), - ) - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - is_active=True, - ).select_related("member") - serializer = ProjectMemberLiteSerializer(members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, issue_id, subscriber_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=subscriber_id, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscribe(self, request, slug, project_id, issue_id): - if IssueSubscriber.objects.filter( - issue_id=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists(): - return Response( - {"message": "User already subscribed to the issue."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - subscriber = IssueSubscriber.objects.create( - issue_id=issue_id, - subscriber_id=request.user.id, - project_id=project_id, - ) - serializer = IssueSubscriberSerializer(subscriber) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def unsubscribe(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=request.user, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscription_status(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.filter( - issue=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists() - return Response( - {"subscribed": issue_subscriber}, status=status.HTTP_200_OK - ) - - -class IssueReactionViewSet(BaseViewSet): - serializer_class = IssueReactionSerializer - model = IssueReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - issue_id=issue_id, - project_id=project_id, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, reaction_code): - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(issue_reaction.id), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class CommentReactionViewSet(BaseViewSet): - serializer_class = CommentReactionSerializer - model = CommentReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(comment_id=self.kwargs.get("comment_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, comment_id): - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - actor_id=request.user.id, - comment_id=comment_id, - ) - issue_activity.delay( - type="comment_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=None, - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, comment_id, reaction_code): - comment_reaction = CommentReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - comment_id=comment_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="comment_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(comment_reaction.id), - "comment_id": str(comment_id), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueRelationViewSet(BaseViewSet): - serializer_class = IssueRelationSerializer - model = IssueRelation - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - issue_relations = ( - IssueRelation.objects.filter( - Q(issue_id=issue_id) | Q(related_issue=issue_id) - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .order_by("-created_at") - .distinct() - ) - - blocking_issues = issue_relations.filter( - relation_type="blocked_by", related_issue_id=issue_id - ) - blocked_by_issues = issue_relations.filter( - relation_type="blocked_by", issue_id=issue_id - ) - duplicate_issues = issue_relations.filter( - issue_id=issue_id, relation_type="duplicate" - ) - duplicate_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="duplicate" - ) - relates_to_issues = issue_relations.filter( - issue_id=issue_id, relation_type="relates_to" - ) - relates_to_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="relates_to" - ) - - blocked_by_issues_serialized = IssueRelationSerializer( - blocked_by_issues, many=True - ).data - duplicate_issues_serialized = IssueRelationSerializer( - duplicate_issues, many=True - ).data - relates_to_issues_serialized = IssueRelationSerializer( - relates_to_issues, many=True - ).data - - # revere relation for blocked by issues - blocking_issues_serialized = RelatedIssueSerializer( - blocking_issues, many=True - ).data - # reverse relation for duplicate issues - duplicate_issues_related_serialized = RelatedIssueSerializer( - duplicate_issues_related, many=True - ).data - # reverse relation for related issues - relates_to_issues_related_serialized = RelatedIssueSerializer( - relates_to_issues_related, many=True - ).data - - response_data = { - "blocking": blocking_issues_serialized, - "blocked_by": blocked_by_issues_serialized, - "duplicate": duplicate_issues_serialized - + duplicate_issues_related_serialized, - "relates_to": relates_to_issues_serialized - + relates_to_issues_related_serialized, - } - - return Response(response_data, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - issues = request.data.get("issues", []) - project = Project.objects.get(pk=project_id) - - issue_relation = IssueRelation.objects.bulk_create( - [ - IssueRelation( - issue_id=( - issue if relation_type == "blocking" else issue_id - ), - related_issue_id=( - issue_id if relation_type == "blocking" else issue - ), - relation_type=( - "blocked_by" - if relation_type == "blocking" - else relation_type - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for issue in issues - ], - batch_size=10, - ignore_conflicts=True, - ) - - issue_activity.delay( - type="issue_relation.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - if relation_type == "blocking": - return Response( - RelatedIssueSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - else: - return Response( - IssueRelationSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - - def remove_relation(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - related_issue = request.data.get("related_issue", None) - - if relation_type == "blocking": - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=related_issue, - related_issue_id=issue_id, - ) - else: - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - related_issue_id=related_issue, - ) - current_instance = json.dumps( - IssueRelationSerializer(issue_relation).data, - cls=DjangoJSONEncoder, - ) - issue_relation.delete() - issue_activity.delay( - type="issue_relation.activity.deleted", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueDraftViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(is_draft=True) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - # Only use serializer when expand else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save(is_draft=True) - - # Track the issue - issue_activity.delay( - type="issue_draft.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset().filter(pk=serializer.data["id"]).first() - ) - return Response( - IssueSerializer(issue).data, status=status.HTTP_201_CREATED - ) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, pk): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue_draft.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(issue).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue_draft.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py new file mode 100644 index 00000000000..ea6e9b3899d --- /dev/null +++ b/apiserver/plane/app/views/issue/activity.py @@ -0,0 +1,85 @@ +# Python imports +from itertools import chain + +# Django imports +from django.db.models import ( + Prefetch, + Q, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + IssueActivity, + IssueComment, + CommentReaction, +) + + +class IssueActivityEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + filters = {} + if request.GET.get("created_at__gt", None) is not None: + filters = {"created_at__gt": request.GET.get("created_at__gt")} + + issue_activities = ( + IssueActivity.objects.filter(issue_id=issue_id) + .filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .select_related("actor", "workspace", "issue", "project") + ).order_by("created_at") + issue_comments = ( + IssueComment.objects.filter(issue_id=issue_id) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .order_by("created_at") + .select_related("actor", "issue", "project", "workspace") + .prefetch_related( + Prefetch( + "comment_reactions", + queryset=CommentReaction.objects.select_related("actor"), + ) + ) + ) + issue_activities = IssueActivitySerializer( + issue_activities, many=True + ).data + issue_comments = IssueCommentSerializer(issue_comments, many=True).data + + if request.GET.get("activity_type", None) == "issue-property": + return Response(issue_activities, status=status.HTTP_200_OK) + + if request.GET.get("activity_type", None) == "issue-comment": + return Response(issue_comments, status=status.HTTP_200_OK) + + result_list = sorted( + chain(issue_activities, issue_comments), + key=lambda instance: instance["created_at"], + ) + + return Response(result_list, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py new file mode 100644 index 00000000000..540715a2414 --- /dev/null +++ b/apiserver/plane/app/views/issue/archive.py @@ -0,0 +1,347 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, +) +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueArchiveViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.annotate( + sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(archived_at__isnull=False) + .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + show_sub_issues = request.GET.get("show_sub_issues", "true") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issue_queryset = ( + issue_queryset + if show_sub_issues == "true" + else issue_queryset.filter(parent__isnull=True) + ) + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def archive(self, request, slug, project_id, pk=None): + issue = Issue.issue_objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + ) + if issue.state.group not in ["completed", "cancelled"]: + return Response( + { + "error": "Can only archive completed or cancelled state group issue" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "archived_at": str(timezone.now().date()), + "automation": False, + } + ), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = timezone.now().date() + issue.save() + + return Response( + {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK + ) + + def unarchive(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, + project_id=project_id, + archived_at__isnull=False, + pk=pk, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": None}), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = None + issue.save() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py new file mode 100644 index 00000000000..c2b8ad6ff7b --- /dev/null +++ b/apiserver/plane/app/views/issue/attachment.py @@ -0,0 +1,73 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueAttachmentSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueAttachment +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueAttachmentEndpoint(BaseAPIView): + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = IssueAttachment + parser_classes = (MultiPartParser, FormParser) + + def post(self, request, slug, project_id, issue_id): + serializer = IssueAttachmentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) + issue_activity.delay( + type="attachment.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.asset.delete(save=False) + issue_attachment.delete() + issue_activity.delay( + type="attachment.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def get(self, request, slug, project_id, issue_id): + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py new file mode 100644 index 00000000000..63d4358b061 --- /dev/null +++ b/apiserver/plane/app/views/issue/base.py @@ -0,0 +1,686 @@ +# Python imports +import json +import random +from itertools import chain + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.db import IntegrityError +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, + IssuePropertySerializer, + IssueSerializer, + IssueCreateSerializer, + LabelSerializer, + IssueFlatSerializer, + IssueLinkSerializer, + IssueLiteSerializer, + IssueAttachmentSerializer, + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, + IssueReactionSerializer, + CommentReactionSerializer, + IssueRelationSerializer, + RelatedIssueSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + WorkSpaceAdminPermission, + ProjectMemberPermission, + ProjectLitePermission, +) +from plane.db.models import ( + Project, + Issue, + IssueActivity, + IssueComment, + IssueProperty, + Label, + IssueLink, + IssueAttachment, + IssueSubscriber, + ProjectMember, + IssueReaction, + CommentReaction, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.grouper import group_results +from plane.utils.issue_filters import issue_filters +from collections import defaultdict +from plane.utils.cache import invalidate_cache + +class IssueListEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + issue_ids = request.GET.get("issues", False) + + if not issue_ids: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issue_ids = [ + issue_id for issue_id in issue_ids.split(",") if issue_id != "" + ] + + queryset = ( + Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = queryset.filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + if self.fields or self.expand: + issues = IssueSerializer( + queryset, many=True, fields=self.fields, expand=self.expand + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + +class IssueViewSet(WebhookMixin, BaseViewSet): + def get_serializer_class(self): + return ( + IssueCreateSerializer + if self.action in ["create", "update", "partial_update"] + else IssueSerializer + ) + + model = Issue + webhook_event = "issue" + permission_classes = [ + ProjectEntityPermission, + ] + + search_fields = [ + "name", + ] + + filterset_fields = [ + "state__name", + "assignees__id", + "workspace__id", + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + # Only use serializer when expand or fields else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save() + + # Track the issue + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() + ) + return Response(issue, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk=None): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + current_instance = json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ) + + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = self.get_queryset().filter(pk=pk).first() + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueUserDisplayPropertyEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id): + issue_property = IssueProperty.objects.get( + user=request.user, + project_id=project_id, + ) + + issue_property.filters = request.data.get( + "filters", issue_property.filters + ) + issue_property.display_filters = request.data.get( + "display_filters", issue_property.display_filters + ) + issue_property.display_properties = request.data.get( + "display_properties", issue_property.display_properties + ) + issue_property.save() + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id): + issue_property, _ = IssueProperty.objects.get_or_create( + user=request.user, project_id=project_id + ) + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class BulkDeleteIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def delete(self, request, slug, project_id): + issue_ids = request.data.get("issue_ids", []) + + if not len(issue_ids): + return Response( + {"error": "Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issues = Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + + total_issues = len(issues) + + issues.delete() + + return Response( + {"message": f"{total_issues} issues were deleted"}, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py new file mode 100644 index 00000000000..eb2d5834ce9 --- /dev/null +++ b/apiserver/plane/app/views/issue/comment.py @@ -0,0 +1,219 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Exists +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueCommentSerializer, + CommentReactionSerializer, +) +from plane.app.permissions import ProjectLitePermission +from plane.db.models import ( + IssueComment, + ProjectMember, + CommentReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueCommentViewSet(WebhookMixin, BaseViewSet): + serializer_class = IssueCommentSerializer + model = IssueComment + webhook_event = "issue_comment" + permission_classes = [ + ProjectLitePermission, + ] + + filterset_fields = [ + "issue__id", + "workspace__id", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + is_active=True, + ) + ) + ) + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueCommentSerializer( + issue_comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + issue_comment.delete() + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentReactionViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, comment_id): + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + actor_id=request.user.id, + comment_id=comment_id, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=None, + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + comment_reaction = CommentReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py new file mode 100644 index 00000000000..08032934bd9 --- /dev/null +++ b/apiserver/plane/app/views/issue/draft.py @@ -0,0 +1,367 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueCreateSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + # Only use serializer when expand else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset().filter(pk=serializer.data["id"]).first() + ) + return Response( + IssueSerializer(issue).data, status=status.HTTP_201_CREATED + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, pk): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py new file mode 100644 index 00000000000..557c2018fe3 --- /dev/null +++ b/apiserver/plane/app/views/issue/label.py @@ -0,0 +1,105 @@ +# Python imports +import random + +# Django imports +from django.db import IntegrityError + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, BaseAPIView +from plane.app.serializers import LabelSerializer +from plane.app.permissions import ( + ProjectMemberPermission, +) +from plane.db.models import ( + Project, + Label, +) +from plane.utils.cache import invalidate_cache + + +class LabelViewSet(BaseViewSet): + serializer_class = LabelSerializer + model = Label + permission_classes = [ + ProjectMemberPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("parent") + .distinct() + .order_by("sort_order") + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def create(self, request, slug, project_id): + try: + serializer = LabelSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError: + return Response( + { + "error": "Label with the same name already exists in the project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class BulkCreateIssueLabelsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + label_data = request.data.get("label_data", []) + project = Project.objects.get(pk=project_id) + + labels = Label.objects.bulk_create( + [ + Label( + name=label.get("name", "Migrated"), + description=label.get("description", "Migrated Issue"), + color="#" + "%06x" % random.randint(0, 0xFFFFFF), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for label in label_data + ], + batch_size=50, + ignore_conflicts=True, + ) + + return Response( + {"labels": LabelSerializer(labels, many=True).data}, + status=status.HTTP_201_CREATED, + ) diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py new file mode 100644 index 00000000000..ca3290759d3 --- /dev/null +++ b/apiserver/plane/app/views/issue/link.py @@ -0,0 +1,120 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueLinkSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueLink +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueLinkViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + model = IssueLink + serializer_class = IssueLinkSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueLinkSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="link.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueLinkSerializer( + issue_link, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="link.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + issue_activity.delay( + type="link.activity.deleted", + requested_data=json.dumps({"link_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py new file mode 100644 index 00000000000..c6f6823be15 --- /dev/null +++ b/apiserver/plane/app/views/issue/reaction.py @@ -0,0 +1,89 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueReactionSerializer +from plane.app.permissions import ProjectLitePermission +from plane.db.models import IssueReaction +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueReactionViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + issue_id=issue_id, + project_id=project_id, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/relation.py b/apiserver/plane/app/views/issue/relation.py new file mode 100644 index 00000000000..45a5dc9a76e --- /dev/null +++ b/apiserver/plane/app/views/issue/relation.py @@ -0,0 +1,204 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Q +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueRelationSerializer, + RelatedIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueRelationViewSet(BaseViewSet): + serializer_class = IssueRelationSerializer + model = IssueRelation + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + issue_relations = ( + IssueRelation.objects.filter( + Q(issue_id=issue_id) | Q(related_issue=issue_id) + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .order_by("-created_at") + .distinct() + ) + + blocking_issues = issue_relations.filter( + relation_type="blocked_by", related_issue_id=issue_id + ) + blocked_by_issues = issue_relations.filter( + relation_type="blocked_by", issue_id=issue_id + ) + duplicate_issues = issue_relations.filter( + issue_id=issue_id, relation_type="duplicate" + ) + duplicate_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="duplicate" + ) + relates_to_issues = issue_relations.filter( + issue_id=issue_id, relation_type="relates_to" + ) + relates_to_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="relates_to" + ) + + blocked_by_issues_serialized = IssueRelationSerializer( + blocked_by_issues, many=True + ).data + duplicate_issues_serialized = IssueRelationSerializer( + duplicate_issues, many=True + ).data + relates_to_issues_serialized = IssueRelationSerializer( + relates_to_issues, many=True + ).data + + # revere relation for blocked by issues + blocking_issues_serialized = RelatedIssueSerializer( + blocking_issues, many=True + ).data + # reverse relation for duplicate issues + duplicate_issues_related_serialized = RelatedIssueSerializer( + duplicate_issues_related, many=True + ).data + # reverse relation for related issues + relates_to_issues_related_serialized = RelatedIssueSerializer( + relates_to_issues_related, many=True + ).data + + response_data = { + "blocking": blocking_issues_serialized, + "blocked_by": blocked_by_issues_serialized, + "duplicate": duplicate_issues_serialized + + duplicate_issues_related_serialized, + "relates_to": relates_to_issues_serialized + + relates_to_issues_related_serialized, + } + + return Response(response_data, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + issues = request.data.get("issues", []) + project = Project.objects.get(pk=project_id) + + issue_relation = IssueRelation.objects.bulk_create( + [ + IssueRelation( + issue_id=( + issue if relation_type == "blocking" else issue_id + ), + related_issue_id=( + issue_id if relation_type == "blocking" else issue + ), + relation_type=( + "blocked_by" + if relation_type == "blocking" + else relation_type + ), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + if relation_type == "blocking": + return Response( + RelatedIssueSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + IssueRelationSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + + def remove_relation(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + related_issue = request.data.get("related_issue", None) + + if relation_type == "blocking": + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=related_issue, + related_issue_id=issue_id, + ) + else: + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + related_issue_id=related_issue, + ) + current_instance = json.dumps( + IssueRelationSerializer(issue_relation).data, + cls=DjangoJSONEncoder, + ) + issue_relation.delete() + issue_activity.delay( + type="issue_relation.activity.deleted", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py new file mode 100644 index 00000000000..6ec4a2de1ff --- /dev/null +++ b/apiserver/plane/app/views/issue/sub_issue.py @@ -0,0 +1,195 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Value, + UUIDField, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from collections import defaultdict + + +class SubIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + sub_issues = ( + Issue.issue_objects.filter( + parent_id=issue_id, workspace__slug=slug + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .annotate(state_group=F("state__group")) + ) + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + sub_issues = sub_issues.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response( + { + "sub_issues": sub_issues, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) + + # Assign multiple sub issues + def post(self, request, slug, project_id, issue_id): + parent_issue = Issue.issue_objects.get(pk=issue_id) + sub_issue_ids = request.data.get("sub_issue_ids", []) + + if not len(sub_issue_ids): + return Response( + {"error": "Sub Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + + for sub_issue in sub_issues: + sub_issue.parent = parent_issue + + _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) + + updated_sub_issues = Issue.issue_objects.filter( + id__in=sub_issue_ids + ).annotate(state_group=F("state__group")) + + # Track the issue + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"parent": str(issue_id)}), + actor_id=str(request.user.id), + issue_id=str(sub_issue_id), + project_id=str(project_id), + current_instance=json.dumps({"parent": str(sub_issue_id)}), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for sub_issue_id in sub_issue_ids + ] + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in updated_sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + serializer = IssueSerializer( + updated_sub_issues, + many=True, + ) + return Response( + { + "sub_issues": serializer.data, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/subscriber.py b/apiserver/plane/app/views/issue/subscriber.py new file mode 100644 index 00000000000..61e09e4a2cd --- /dev/null +++ b/apiserver/plane/app/views/issue/subscriber.py @@ -0,0 +1,124 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, +) +from plane.db.models import ( + IssueSubscriber, + ProjectMember, +) + + +class IssueSubscriberViewSet(BaseViewSet): + serializer_class = IssueSubscriberSerializer + model = IssueSubscriber + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_permissions(self): + if self.action in ["subscribe", "unsubscribe", "subscription_status"]: + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectEntityPermission, + ] + + return super(IssueSubscriberViewSet, self).get_permissions() + + def perform_create(self, serializer): + serializer.save( + project_id=self.kwargs.get("project_id"), + issue_id=self.kwargs.get("issue_id"), + ) + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + is_active=True, + ).select_related("member") + serializer = ProjectMemberLiteSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, issue_id, subscriber_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscribe(self, request, slug, project_id, issue_id): + if IssueSubscriber.objects.filter( + issue_id=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists(): + return Response( + {"message": "User already subscribed to the issue."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + subscriber = IssueSubscriber.objects.create( + issue_id=issue_id, + subscriber_id=request.user.id, + project_id=project_id, + ) + serializer = IssueSubscriberSerializer(subscriber) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def unsubscribe(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=request.user, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscription_status(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.filter( + issue=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists() + return Response( + {"subscribed": issue_subscriber}, status=status.HTTP_200_OK + ) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module/base.py similarity index 67% rename from apiserver/plane/app/views/module.py rename to apiserver/plane/app/views/module/base.py index c93e0b01c7d..cd87442d2f6 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module/base.py @@ -3,9 +3,7 @@ # Django Imports from django.utils import timezone -from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page +from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import Value, UUIDField @@ -16,14 +14,12 @@ from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin +from .. import BaseViewSet, BaseAPIView, WebhookMixin from plane.app.serializers import ( ModuleWriteSerializer, ModuleSerializer, - ModuleIssueSerializer, ModuleLinkSerializer, ModuleFavoriteSerializer, - IssueSerializer, ModuleUserPropertiesSerializer, ModuleDetailSerializer, ) @@ -38,12 +34,9 @@ Issue, ModuleLink, ModuleFavorite, - IssueLink, - IssueAttachment, ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot @@ -426,232 +419,6 @@ def destroy(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class ModuleIssueViewSet(WebhookMixin, BaseViewSet): - serializer_class = ModuleIssueSerializer - model = ModuleIssue - webhook_event = "module_issue" - bulk = True - - filterset_fields = [ - "issue__labels__id", - "issue__assignees__id", - ] - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - workspace__slug=self.kwargs.get("slug"), - issue_module__module_id=self.kwargs.get("module_id"), - ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id, module_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - filters = issue_filters(request.query_params, "GET") - issue_queryset = self.get_queryset().filter(**filters) - if self.fields or self.expand: - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) - - # create multiple issues inside a module - def create_module_issues(self, request, slug, project_id, module_id): - issues = request.data.get("issues", []) - if not issues: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - project = Project.objects.get(pk=project_id) - _ = ModuleIssue.objects.bulk_create( - [ - ModuleIssue( - issue_id=str(issue), - module_id=module_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for issue in issues - ], - batch_size=10, - ignore_conflicts=True, - ) - # Bulk Update the activity - _ = [ - issue_activity.delay( - type="module.activity.created", - requested_data=json.dumps({"module_id": str(module_id)}), - actor_id=str(request.user.id), - issue_id=str(issue), - project_id=project_id, - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for issue in issues - ] - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - # create multiple module inside an issue - def create_issue_modules(self, request, slug, project_id, issue_id): - modules = request.data.get("modules", []) - if not modules: - return Response( - {"error": "Modules are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - project = Project.objects.get(pk=project_id) - _ = ModuleIssue.objects.bulk_create( - [ - ModuleIssue( - issue_id=issue_id, - module_id=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for module in modules - ], - batch_size=10, - ignore_conflicts=True, - ) - # Bulk Update the activity - _ = [ - issue_activity.delay( - type="module.activity.created", - requested_data=json.dumps({"module_id": module}), - actor_id=str(request.user.id), - issue_id=issue_id, - project_id=project_id, - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for module in modules - ] - - return Response({"message": "success"}, status=status.HTTP_201_CREATED) - - def destroy(self, request, slug, project_id, module_id, issue_id): - module_issue = ModuleIssue.objects.get( - workspace__slug=slug, - project_id=project_id, - module_id=module_id, - issue_id=issue_id, - ) - issue_activity.delay( - type="module.activity.deleted", - requested_data=json.dumps({"module_id": str(module_id)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=json.dumps( - {"module_name": module_issue.module.name} - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - module_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - class ModuleLinkViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py new file mode 100644 index 00000000000..cfa8ee478c6 --- /dev/null +++ b/apiserver/plane/app/views/module/issue.py @@ -0,0 +1,259 @@ +# Python imports +import json + +# Django Imports +from django.utils import timezone +from django.db.models import F, OuterRef, Func, Q +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + ModuleIssueSerializer, + IssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + ModuleIssue, + Project, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class ModuleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = ModuleIssueSerializer + model = ModuleIssue + webhook_event = "module_issue" + bulk = True + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + issue_module__module_id=self.kwargs.get("module_id"), + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, module_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + issue_queryset = self.get_queryset().filter(**filters) + if self.fields or self.expand: + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + # create multiple issues inside a module + def create_module_issues(self, request, slug, project_id, module_id): + issues = request.data.get("issues", []) + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=str(issue), + module_id=module_id, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue), + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for issue in issues + ] + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + # create multiple module inside an issue + def create_issue_modules(self, request, slug, project_id, issue_id): + modules = request.data.get("modules", []) + if not modules: + return Response( + {"error": "Modules are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=issue_id, + module_id=module, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for module in modules + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": module}), + actor_id=str(request.user.id), + issue_id=issue_id, + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for module in modules + ] + + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, module_id, issue_id): + module_issue = ModuleIssue.objects.get( + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="module.activity.deleted", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + {"module_name": module_issue.module.name} + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + module_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification/base.py similarity index 99% rename from apiserver/plane/app/views/notification.py rename to apiserver/plane/app/views/notification/base.py index a6f84f65a6b..8dae618dbcf 100644 --- a/apiserver/plane/app/views/notification.py +++ b/apiserver/plane/app/views/notification/base.py @@ -8,7 +8,7 @@ from plane.utils.paginator import BasePaginator # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.db.models import ( Notification, IssueAssignee, diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page/base.py similarity index 99% rename from apiserver/plane/app/views/page.py rename to apiserver/plane/app/views/page/base.py index 21d461fe16c..34a9ee638c4 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page/base.py @@ -26,7 +26,7 @@ ) # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet def unarchive_archive_page_and_descendants(page_id, archived_at): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py deleted file mode 100644 index d1f0159af03..00000000000 --- a/apiserver/plane/app/views/project.py +++ /dev/null @@ -1,1146 +0,0 @@ -# Python imports -from datetime import datetime - -import boto3 -import jwt -from django.conf import settings - -# Django imports -from django.core.exceptions import ValidationError -from django.core.validators import validate_email -from django.db import IntegrityError -from django.db.models import ( - Exists, - F, - Func, - OuterRef, - Prefetch, - Q, - Subquery, -) -from django.utils import timezone - -# Third Party imports -from rest_framework import serializers, status -from rest_framework.permissions import AllowAny -from rest_framework.response import Response - -# Module imports -from plane.app.permissions import ( - ProjectBasePermission, - ProjectLitePermission, - ProjectMemberPermission, - WorkspaceUserPermission, -) -from plane.app.serializers import ( - ProjectDeployBoardSerializer, - ProjectFavoriteSerializer, - ProjectListSerializer, - ProjectMemberAdminSerializer, - ProjectMemberInviteSerializer, - ProjectMemberRoleSerializer, - ProjectMemberSerializer, - ProjectSerializer, -) -from plane.bgtasks.project_invitation_task import project_invitation -from plane.db.models import ( - Cycle, - Inbox, - IssueProperty, - Module, - Project, - ProjectDeployBoard, - ProjectFavorite, - ProjectIdentifier, - ProjectMember, - ProjectMemberInvite, - State, - TeamMember, - User, - Workspace, - WorkspaceMember, -) -from plane.utils.cache import cache_response - -from .base import BaseAPIView, BaseViewSet, WebhookMixin - - -class ProjectViewSet(WebhookMixin, BaseViewSet): - serializer_class = ProjectListSerializer - model = Project - webhook_event = "project" - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - sort_order = ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).values("sort_order") - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter( - Q(project_projectmember__member=self.request.user) - | Q(network=2) - ) - .select_related( - "workspace", - "workspace__owner", - "default_assignee", - "project_lead", - ) - .annotate( - is_favorite=Exists( - ProjectFavorite.objects.filter( - user=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - ) - ) - .annotate( - total_members=ProjectMember.objects.filter( - project_id=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_modules=Module.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - member_role=ProjectMember.objects.filter( - project_id=OuterRef("pk"), - member_id=self.request.user.id, - is_active=True, - ).values("role") - ) - .annotate( - is_deployed=Exists( - ProjectDeployBoard.objects.filter( - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate(sort_order=Subquery(sort_order)) - .prefetch_related( - Prefetch( - "project_projectmember", - queryset=ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).select_related("member"), - to_attr="members_list", - ) - ) - .distinct() - ) - - def list(self, request, slug): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - projects = self.get_queryset().order_by("sort_order", "name") - if request.GET.get("per_page", False) and request.GET.get( - "cursor", False - ): - return self.paginate( - request=request, - queryset=(projects), - on_results=lambda projects: ProjectListSerializer( - projects, many=True - ).data, - ) - projects = ProjectListSerializer( - projects, many=True, fields=fields if fields else None - ).data - return Response(projects, status=status.HTTP_200_OK) - - def create(self, request, slug): - try: - workspace = Workspace.objects.get(slug=slug) - - serializer = ProjectSerializer( - data={**request.data}, context={"workspace_id": workspace.id} - ) - if serializer.is_valid(): - serializer.save() - - # Add the user as Administrator to the project - _ = ProjectMember.objects.create( - project_id=serializer.data["id"], - member=request.user, - role=20, - ) - # Also create the issue property for the user - _ = IssueProperty.objects.create( - project_id=serializer.data["id"], - user=request.user, - ) - - if serializer.data["project_lead"] is not None and str( - serializer.data["project_lead"] - ) != str(request.user.id): - ProjectMember.objects.create( - project_id=serializer.data["id"], - member_id=serializer.data["project_lead"], - role=20, - ) - # Also create the issue property for the user - IssueProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) - - # Default states - states = [ - { - "name": "Backlog", - "color": "#A3A3A3", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#3A3A3A", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#16A34A", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#EF4444", - "sequence": 55000, - "group": "cancelled", - }, - ] - - State.objects.bulk_create( - [ - State( - name=state["name"], - color=state["color"], - project=serializer.instance, - sequence=state["sequence"], - workspace=serializer.instance.workspace, - group=state["group"], - default=state.get("default", False), - created_by=request.user, - ) - for state in states - ] - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST, - ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except Workspace.DoesNotExist: - return Response( - {"error": "Workspace does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - def partial_update(self, request, slug, pk=None): - try: - workspace = Workspace.objects.get(slug=slug) - - project = Project.objects.get(pk=pk) - - serializer = ProjectSerializer( - project, - data={**request.data}, - context={"workspace_id": workspace.id}, - partial=True, - ) - - if serializer.is_valid(): - serializer.save() - if serializer.data["inbox_view"]: - Inbox.objects.get_or_create( - name=f"{project.name} Inbox", - project=project, - is_default=True, - ) - - # Create the triage state in Backlog group - State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=pk, - color="#ff7700", - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except (Project.DoesNotExist, Workspace.DoesNotExist): - return Response( - {"error": "Project does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - -class ProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - search_fields = [] - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .select_related("project") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - emails = request.data.get("emails", []) - - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - requesting_user = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member_id=request.user.id, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - project_invitations.append( - ProjectMemberInvite( - email=email.get("email").strip().lower(), - project_id=project_id, - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Create workspace member invite - project_invitations = ProjectMemberInvite.objects.bulk_create( - project_invitations, batch_size=10, ignore_conflicts=True - ) - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in project_invitations: - project_invitations.delay( - invitation.email, - project_id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Email sent successfully", - }, - status=status.HTTP_200_OK, - ) - - -class UserProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "project") - ) - - def create(self, request, slug): - project_ids = request.data.get("project_ids", []) - - # Get the workspace user role - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - workspace_role = workspace_member.role - workspace = workspace_member.workspace - - # If the user was already part of workspace - _ = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - member=request.user, - ).update(is_active=True) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=project_id, - member=request.user, - role=15 if workspace_role >= 15 else 10, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=project_id, - user=request.user, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - return Response( - {"message": "Projects joined successfully"}, - status=status.HTTP_201_CREATED, - ) - - -class ProjectJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def post(self, request, slug, project_id, pk): - project_invite = ProjectMemberInvite.objects.get( - pk=pk, - project_id=project_id, - workspace__slug=slug, - ) - - email = request.data.get("email", "") - - if email == "" or project_invite.email != email: - return Response( - {"error": "You do not have permission to join the project"}, - status=status.HTTP_403_FORBIDDEN, - ) - - if project_invite.responded_at is None: - project_invite.accepted = request.data.get("accepted", False) - project_invite.responded_at = timezone.now() - project_invite.save() - - if project_invite.accepted: - # Check if the user account exists - user = User.objects.filter(email=email).first() - - # Check if user is a part of workspace - workspace_member = WorkspaceMember.objects.filter( - workspace__slug=slug, member=user - ).first() - # Add him to workspace - if workspace_member is None: - _ = WorkspaceMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=( - 15 - if project_invite.role >= 15 - else project_invite.role - ), - ) - else: - # Else make him active - workspace_member.is_active = True - workspace_member.save() - - # Check if the user was already a member of project then activate the user - project_member = ProjectMember.objects.filter( - workspace_id=project_invite.workspace_id, member=user - ).first() - if project_member is None: - # Create a Project Member - _ = ProjectMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=project_invite.role, - ) - else: - project_member.is_active = True - project_member.role = project_member.role - project_member.save() - - return Response( - {"message": "Project Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"message": "Project Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, project_id, pk): - project_invitation = ProjectMemberInvite.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - serializer = ProjectMemberInviteSerializer(project_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectMemberViewSet(BaseViewSet): - serializer_class = ProjectMemberAdminSerializer - model = ProjectMember - permission_classes = [ - ProjectMemberPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectMemberPermission, - ] - - return super(ProjectMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(member__is_bot=False) - .filter() - .select_related("project") - .select_related("member") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - members = request.data.get("members", []) - - # get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - if not len(members): - return Response( - {"error": "Atleast one member is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_project_members = [] - bulk_issue_props = [] - - project_members = ( - ProjectMember.objects.filter( - workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], - ) - .values("member_id", "sort_order") - .order_by("sort_order") - ) - - bulk_project_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], - ): - project_member.role = member_roles[str(project_member.member_id)] - project_member.is_active = True - bulk_project_members.append(project_member) - - # Update the roles of the existing members - ProjectMember.objects.bulk_update( - bulk_project_members, ["is_active", "role"], batch_size=100 - ) - - for member in members: - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) - == str(member.get("member_id")) - ] - bulk_project_members.append( - ProjectMember( - member_id=member.get("member_id"), - 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 - ), - ) - ) - bulk_issue_props.append( - IssueProperty( - user_id=member.get("member_id"), - project_id=project_id, - workspace_id=project.workspace_id, - ) - ) - - project_members = ProjectMember.objects.bulk_create( - bulk_project_members, - batch_size=10, - ignore_conflicts=True, - ) - - _ = IssueProperty.objects.bulk_create( - 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], - ) - serializer = ProjectMemberRoleSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def list(self, request, slug, project_id): - # Get the list of project members for the project - project_members = ProjectMember.objects.filter( - project_id=project_id, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ).select_related("project", "member", "workspace") - - serializer = ProjectMemberRoleSerializer( - project_members, fields=("id", "member", "role"), many=True - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - pk=pk, - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - if request.user.id == project_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Check while updating user roles - requested_project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - if ( - "role" in request.data - and int(request.data.get("role", project_member.role)) - > requested_project_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = ProjectMemberSerializer( - project_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - member__is_bot=False, - is_active=True, - ) - # check requesting user role - requesting_project_member = ProjectMember.objects.get( - workspace__slug=slug, - member=request.user, - project_id=project_id, - is_active=True, - ) - # User cannot remove himself - if str(project_member.id) == str(requesting_project_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # User cannot deactivate higher role - if requesting_project_member.role < project_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - def leave(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the project - if ( - project_member.role == 20 - and not ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Deactivate the user - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AddTeamToProjectEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def post(self, request, slug, project_id): - team_members = TeamMember.objects.filter( - workspace__slug=slug, team__in=request.data.get("teams", []) - ).values_list("member", flat=True) - - if len(team_members) == 0: - return Response( - {"error": "No such team exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_members = [] - issue_props = [] - for member in team_members: - project_members.append( - ProjectMember( - project_id=project_id, - member_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - issue_props.append( - IssueProperty( - project_id=project_id, - user_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - - ProjectMember.objects.bulk_create( - project_members, batch_size=10, ignore_conflicts=True - ) - - _ = IssueProperty.objects.bulk_create( - issue_props, batch_size=10, ignore_conflicts=True - ) - - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class ProjectIdentifierEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug): - name = request.GET.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - exists = ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).values("id", "name", "project") - - return Response( - {"exists": len(exists), "identifiers": exists}, - status=status.HTTP_200_OK, - ) - - def delete(self, request, slug): - name = request.data.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if Project.objects.filter( - identifier=name, workspace__slug=slug - ).exists(): - return Response( - { - "error": "Cannot delete an identifier of an existing project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).delete() - - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - -class ProjectUserViewsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - project_member = ProjectMember.objects.filter( - member=request.user, - project=project, - is_active=True, - ).first() - - if project_member is None: - return Response( - {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN - ) - - view_props = project_member.view_props - default_props = project_member.default_props - preferences = project_member.preferences - sort_order = project_member.sort_order - - project_member.view_props = request.data.get("view_props", view_props) - project_member.default_props = request.data.get( - "default_props", default_props - ) - project_member.preferences = request.data.get( - "preferences", preferences - ) - project_member.sort_order = request.data.get("sort_order", sort_order) - - project_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectMemberUserEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - serializer = ProjectMemberSerializer(project_member) - - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectFavoritesViewSet(BaseViewSet): - serializer_class = ProjectFavoriteSerializer - model = ProjectFavorite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(user=self.request.user) - .select_related( - "project", "project__project_lead", "project__default_assignee" - ) - .select_related("workspace", "workspace__owner") - ) - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - def create(self, request, slug): - serializer = ProjectFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id): - project_favorite = ProjectFavorite.objects.get( - project=project_id, user=request.user, workspace__slug=slug - ) - project_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectPublicCoverImagesEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - # Cache the below api for 24 hours - @cache_response(60 * 60 * 24, user=False) - def get(self, request): - files = [] - s3 = boto3.client( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - ) - params = { - "Bucket": settings.AWS_STORAGE_BUCKET_NAME, - "Prefix": "static/project-cover/", - } - - response = s3.list_objects_v2(**params) - # Extracting file keys from the response - if "Contents" in response: - for content in response["Contents"]: - if not content["Key"].endswith( - "/" - ): # This line ensures we're only getting files, not "sub-folders" - files.append( - f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) - - return Response(files, status=status.HTTP_200_OK) - - -class ProjectDeployBoardViewSet(BaseViewSet): - permission_classes = [ - ProjectMemberPermission, - ] - serializer_class = ProjectDeployBoardSerializer - model = ProjectDeployBoard - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .select_related("project") - ) - - def create(self, request, slug, project_id): - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) - inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) - views = request.data.get( - "views", - { - "list": True, - "kanban": True, - "calendar": True, - "gantt": True, - "spreadsheet": True, - }, - ) - - project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", - project_id=project_id, - ) - project_deploy_board.comments = comments - project_deploy_board.reactions = reactions - project_deploy_board.inbox = inbox - project_deploy_board.votes = votes - project_deploy_board.views = views - - project_deploy_board.save() - - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserProjectRolesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceUserPermission, - ] - - def get(self, request, slug): - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=request.user.id, - ).values("project_id", "role") - - project_members = { - str(member["project_id"]): member["role"] - for member in project_members - } - return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py new file mode 100644 index 00000000000..6deeea1447c --- /dev/null +++ b/apiserver/plane/app/views/project/base.py @@ -0,0 +1,549 @@ +# Python imports +import boto3 + +# Django imports +from django.db import IntegrityError +from django.db.models import ( + Prefetch, + Q, + Exists, + OuterRef, + F, + Func, + Subquery, +) +from django.conf import settings + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework import serializers +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + ProjectSerializer, + ProjectListSerializer, + ProjectFavoriteSerializer, + ProjectDeployBoardSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + State, + ProjectFavorite, + ProjectIdentifier, + Module, + Cycle, + Inbox, + ProjectDeployBoard, + IssueProperty, +) +from plane.utils.cache import cache_response + +class ProjectViewSet(WebhookMixin, BaseViewSet): + serializer_class = ProjectListSerializer + model = Project + webhook_event = "project" + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + sort_order = ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).values("sort_order") + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter( + Q(project_projectmember__member=self.request.user) + | Q(network=2) + ) + .select_related( + "workspace", + "workspace__owner", + "default_assignee", + "project_lead", + ) + .annotate( + is_favorite=Exists( + ProjectFavorite.objects.filter( + user=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + ) + ) + .annotate( + total_members=ProjectMember.objects.filter( + project_id=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_modules=Module.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + member_role=ProjectMember.objects.filter( + project_id=OuterRef("pk"), + member_id=self.request.user.id, + is_active=True, + ).values("role") + ) + .annotate( + is_deployed=Exists( + ProjectDeployBoard.objects.filter( + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate(sort_order=Subquery(sort_order)) + .prefetch_related( + Prefetch( + "project_projectmember", + queryset=ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).select_related("member"), + to_attr="members_list", + ) + ) + .distinct() + ) + + def list(self, request, slug): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + projects = self.get_queryset().order_by("sort_order", "name") + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): + return self.paginate( + request=request, + queryset=(projects), + on_results=lambda projects: ProjectListSerializer( + projects, many=True + ).data, + ) + projects = ProjectListSerializer( + projects, many=True, fields=fields if fields else None + ).data + return Response(projects, status=status.HTTP_200_OK) + + def create(self, request, slug): + try: + workspace = Workspace.objects.get(slug=slug) + + serializer = ProjectSerializer( + data={**request.data}, context={"workspace_id": workspace.id} + ) + if serializer.is_valid(): + serializer.save() + + # Add the user as Administrator to the project + _ = ProjectMember.objects.create( + project_id=serializer.data["id"], + member=request.user, + role=20, + ) + # Also create the issue property for the user + _ = IssueProperty.objects.create( + project_id=serializer.data["id"], + user=request.user, + ) + + if serializer.data["project_lead"] is not None and str( + serializer.data["project_lead"] + ) != str(request.user.id): + ProjectMember.objects.create( + project_id=serializer.data["id"], + member_id=serializer.data["project_lead"], + role=20, + ) + # Also create the issue property for the user + IssueProperty.objects.create( + project_id=serializer.data["id"], + user_id=serializer.data["project_lead"], + ) + + # Default states + states = [ + { + "name": "Backlog", + "color": "#A3A3A3", + "sequence": 15000, + "group": "backlog", + "default": True, + }, + { + "name": "Todo", + "color": "#3A3A3A", + "sequence": 25000, + "group": "unstarted", + }, + { + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": "started", + }, + { + "name": "Done", + "color": "#16A34A", + "sequence": 45000, + "group": "completed", + }, + { + "name": "Cancelled", + "color": "#EF4444", + "sequence": 55000, + "group": "cancelled", + }, + ] + + State.objects.bulk_create( + [ + State( + name=state["name"], + color=state["color"], + project=serializer.instance, + sequence=state["sequence"], + workspace=serializer.instance.workspace, + group=state["group"], + default=state.get("default", False), + created_by=request.user, + ) + for state in states + ] + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except Workspace.DoesNotExist as e: + return Response( + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + def partial_update(self, request, slug, pk=None): + try: + workspace = Workspace.objects.get(slug=slug) + + project = Project.objects.get(pk=pk) + + serializer = ProjectSerializer( + project, + data={**request.data}, + context={"workspace_id": workspace.id}, + partial=True, + ) + + if serializer.is_valid(): + serializer.save() + if serializer.data["inbox_view"]: + Inbox.objects.get_or_create( + name=f"{project.name} Inbox", + project=project, + is_default=True, + ) + + # Create the triage state in Backlog group + State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=pk, + color="#ff7700", + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except (Project.DoesNotExist, Workspace.DoesNotExist): + return Response( + {"error": "Project does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + +class ProjectIdentifierEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def get(self, request, slug): + name = request.GET.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + exists = ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).values("id", "name", "project") + + return Response( + {"exists": len(exists), "identifiers": exists}, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug): + name = request.data.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if Project.objects.filter( + identifier=name, workspace__slug=slug + ).exists(): + return Response( + { + "error": "Cannot delete an identifier of an existing project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).delete() + + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + +class ProjectUserViewsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + project_member = ProjectMember.objects.filter( + member=request.user, + project=project, + is_active=True, + ).first() + + if project_member is None: + return Response( + {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN + ) + + view_props = project_member.view_props + default_props = project_member.default_props + preferences = project_member.preferences + sort_order = project_member.sort_order + + project_member.view_props = request.data.get("view_props", view_props) + project_member.default_props = request.data.get( + "default_props", default_props + ) + project_member.preferences = request.data.get( + "preferences", preferences + ) + project_member.sort_order = request.data.get("sort_order", sort_order) + + project_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectFavoritesViewSet(BaseViewSet): + serializer_class = ProjectFavoriteSerializer + model = ProjectFavorite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(user=self.request.user) + .select_related( + "project", "project__project_lead", "project__default_assignee" + ) + .select_related("workspace", "workspace__owner") + ) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, slug): + serializer = ProjectFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id): + project_favorite = ProjectFavorite.objects.get( + project=project_id, user=request.user, workspace__slug=slug + ) + project_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectPublicCoverImagesEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + # Cache the below api for 24 hours + @cache_response(60 * 60 * 24, user=False) + def get(self, request): + files = [] + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + # Extracting file keys from the response + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith( + "/" + ): # This line ensures we're only getting files, not "sub-folders" + files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return Response(files, status=status.HTTP_200_OK) + + +class ProjectDeployBoardViewSet(BaseViewSet): + permission_classes = [ + ProjectMemberPermission, + ] + serializer_class = ProjectDeployBoardSerializer + model = ProjectDeployBoard + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + .select_related("project") + ) + + def create(self, request, slug, project_id): + comments = request.data.get("comments", False) + reactions = request.data.get("reactions", False) + inbox = request.data.get("inbox", None) + votes = request.data.get("votes", False) + views = request.data.get( + "views", + { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + }, + ) + + project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( + anchor=f"{slug}/{project_id}", + project_id=project_id, + ) + project_deploy_board.comments = comments + project_deploy_board.reactions = reactions + project_deploy_board.inbox = inbox + project_deploy_board.votes = votes + project_deploy_board.views = views + + project_deploy_board.save() + + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py new file mode 100644 index 00000000000..d199a877004 --- /dev/null +++ b/apiserver/plane/app/views/project/invite.py @@ -0,0 +1,286 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.conf import settings +from django.utils import timezone + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ProjectMemberInviteSerializer + +from plane.app.permissions import ProjectBasePermission + +from plane.db.models import ( + ProjectMember, + Workspace, + ProjectMemberInvite, + User, + WorkspaceMember, + IssueProperty, +) + + +class ProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + search_fields = [] + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .select_related("project") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + emails = request.data.get("emails", []) + + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + requesting_user = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member_id=request.user.id, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + project_invitations.append( + ProjectMemberInvite( + email=email.get("email").strip().lower(), + project_id=project_id, + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create workspace member invite + project_invitations = ProjectMemberInvite.objects.bulk_create( + project_invitations, batch_size=10, ignore_conflicts=True + ) + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in project_invitations: + project_invitations.delay( + invitation.email, + project_id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Email sent successfully", + }, + status=status.HTTP_200_OK, + ) + + +class UserProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "project") + ) + + def create(self, request, slug): + project_ids = request.data.get("project_ids", []) + + # Get the workspace user role + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + workspace_role = workspace_member.role + workspace = workspace_member.workspace + + # If the user was already part of workspace + _ = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + member=request.user, + ).update(is_active=True) + + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project_id=project_id, + member=request.user, + role=15 if workspace_role >= 15 else 10, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + IssueProperty.objects.bulk_create( + [ + IssueProperty( + project_id=project_id, + user=request.user, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + return Response( + {"message": "Projects joined successfully"}, + status=status.HTTP_201_CREATED, + ) + + +class ProjectJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request, slug, project_id, pk): + project_invite = ProjectMemberInvite.objects.get( + pk=pk, + project_id=project_id, + workspace__slug=slug, + ) + + email = request.data.get("email", "") + + if email == "" or project_invite.email != email: + return Response( + {"error": "You do not have permission to join the project"}, + status=status.HTTP_403_FORBIDDEN, + ) + + if project_invite.responded_at is None: + project_invite.accepted = request.data.get("accepted", False) + project_invite.responded_at = timezone.now() + project_invite.save() + + if project_invite.accepted: + # Check if the user account exists + user = User.objects.filter(email=email).first() + + # Check if user is a part of workspace + workspace_member = WorkspaceMember.objects.filter( + workspace__slug=slug, member=user + ).first() + # Add him to workspace + if workspace_member is None: + _ = WorkspaceMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=( + 15 + if project_invite.role >= 15 + else project_invite.role + ), + ) + else: + # Else make him active + workspace_member.is_active = True + workspace_member.save() + + # Check if the user was already a member of project then activate the user + project_member = ProjectMember.objects.filter( + workspace_id=project_invite.workspace_id, member=user + ).first() + if project_member is None: + # Create a Project Member + _ = ProjectMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=project_invite.role, + ) + else: + project_member.is_active = True + project_member.role = project_member.role + project_member.save() + + return Response( + {"message": "Project Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"message": "Project Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, project_id, pk): + project_invitation = ProjectMemberInvite.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + serializer = ProjectMemberInviteSerializer(project_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py new file mode 100644 index 00000000000..187dfc8d05c --- /dev/null +++ b/apiserver/plane/app/views/project/member.py @@ -0,0 +1,349 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ( + ProjectMemberSerializer, + ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, + ProjectLitePermission, + WorkspaceUserPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + TeamMember, + IssueProperty, +) + + +class ProjectMemberViewSet(BaseViewSet): + serializer_class = ProjectMemberAdminSerializer + model = ProjectMember + permission_classes = [ + ProjectMemberPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectMemberPermission, + ] + + return super(ProjectMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(member__is_bot=False) + .filter() + .select_related("project") + .select_related("member") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + members = request.data.get("members", []) + + # get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + if not len(members): + return Response( + {"error": "Atleast one member is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + bulk_project_members = [] + bulk_issue_props = [] + + project_members = ( + ProjectMember.objects.filter( + workspace__slug=slug, + member_id__in=[member.get("member_id") for member in members], + ) + .values("member_id", "sort_order") + .order_by("sort_order") + ) + + bulk_project_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], + ): + project_member.role = member_roles[str(project_member.member_id)] + project_member.is_active = True + bulk_project_members.append(project_member) + + # Update the roles of the existing members + ProjectMember.objects.bulk_update( + bulk_project_members, ["is_active", "role"], batch_size=100 + ) + + for member in members: + sort_order = [ + project_member.get("sort_order") + for project_member in project_members + if str(project_member.get("member_id")) + == str(member.get("member_id")) + ] + bulk_project_members.append( + ProjectMember( + member_id=member.get("member_id"), + 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 + ), + ) + ) + bulk_issue_props.append( + IssueProperty( + user_id=member.get("member_id"), + project_id=project_id, + workspace_id=project.workspace_id, + ) + ) + + project_members = ProjectMember.objects.bulk_create( + bulk_project_members, + batch_size=10, + ignore_conflicts=True, + ) + + _ = IssueProperty.objects.bulk_create( + 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], + ) + serializer = ProjectMemberRoleSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def list(self, request, slug, project_id): + # Get the list of project members for the project + project_members = ProjectMember.objects.filter( + project_id=project_id, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ).select_related("project", "member", "workspace") + + serializer = ProjectMemberRoleSerializer( + project_members, fields=("id", "member", "role"), many=True + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + pk=pk, + workspace__slug=slug, + project_id=project_id, + is_active=True, + ) + if request.user.id == project_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + # Check while updating user roles + requested_project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) + > requested_project_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ProjectMemberSerializer( + project_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + member__is_bot=False, + is_active=True, + ) + # check requesting user role + requesting_project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + is_active=True, + ) + # User cannot remove himself + if str(project_member.id) == str(requesting_project_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # User cannot deactivate higher role + if requesting_project_member.role < project_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + def leave(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the project + if ( + project_member.role == 20 + and not ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Deactivate the user + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AddTeamToProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def post(self, request, slug, project_id): + team_members = TeamMember.objects.filter( + workspace__slug=slug, team__in=request.data.get("teams", []) + ).values_list("member", flat=True) + + if len(team_members) == 0: + return Response( + {"error": "No such team exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_members = [] + issue_props = [] + for member in team_members: + project_members.append( + ProjectMember( + project_id=project_id, + member_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + issue_props.append( + IssueProperty( + project_id=project_id, + user_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + + ProjectMember.objects.bulk_create( + project_members, batch_size=10, ignore_conflicts=True + ) + + _ = IssueProperty.objects.bulk_create( + issue_props, batch_size=10, ignore_conflicts=True + ) + + serializer = ProjectMemberSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class ProjectMemberUserEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + serializer = ProjectMemberSerializer(project_member) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserProjectRolesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceUserPermission, + ] + + def get(self, request, slug): + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=request.user.id, + ).values("project_id", "role") + + project_members = { + str(member["project_id"]): member["role"] + for member in project_members + } + return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state/base.py similarity index 99% rename from apiserver/plane/app/views/state.py rename to apiserver/plane/app/views/state/base.py index 6d4fd778274..137a89d9916 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state/base.py @@ -9,7 +9,7 @@ from rest_framework import status # Module imports -from . import BaseViewSet +from .. import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( ProjectEntityPermission, diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user/base.py similarity index 100% rename from apiserver/plane/app/views/user.py rename to apiserver/plane/app/views/user/base.py diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view/base.py similarity index 99% rename from apiserver/plane/app/views/view.py rename to apiserver/plane/app/views/view/base.py index 3461f78f682..16c50e8805f 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view/base.py @@ -23,7 +23,7 @@ from rest_framework import status # Module imports -from . import BaseViewSet +from .. import BaseViewSet from plane.app.serializers import ( IssueViewSerializer, IssueSerializer, diff --git a/apiserver/plane/app/views/webhook.py b/apiserver/plane/app/views/webhook/base.py similarity index 99% rename from apiserver/plane/app/views/webhook.py rename to apiserver/plane/app/views/webhook/base.py index 6c110eea3a8..9586722a0cc 100644 --- a/apiserver/plane/app/views/webhook.py +++ b/apiserver/plane/app/views/webhook/base.py @@ -8,7 +8,7 @@ # Module imports from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import WorkspaceOwnerPermission from plane.app.serializers import WebhookSerializer, WebhookLogSerializer diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py deleted file mode 100644 index 41990106270..00000000000 --- a/apiserver/plane/app/views/workspace.py +++ /dev/null @@ -1,1843 +0,0 @@ -# Python imports -import jwt -import csv -import io -from datetime import date, datetime -from dateutil.relativedelta import relativedelta - -# Django imports -from django.http import HttpResponse -from django.db import IntegrityError -from django.conf import settings -from django.utils import timezone -from django.core.exceptions import ValidationError -from django.core.validators import validate_email -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Count, - Case, - Value, - CharField, - When, - Max, - IntegerField, - Sum, -) -from django.db.models.functions import ExtractWeek, Cast, ExtractDay -from django.db.models.fields import DateField -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import UUIDField -from django.db.models.functions import Coalesce - -# Third party modules -from rest_framework import status -from rest_framework.response import Response -from rest_framework.permissions import AllowAny - -# Module imports -from plane.app.serializers import ( - WorkSpaceSerializer, - WorkSpaceMemberSerializer, - TeamSerializer, - WorkSpaceMemberInviteSerializer, - UserLiteSerializer, - ProjectMemberSerializer, - WorkspaceThemeSerializer, - IssueActivitySerializer, - IssueSerializer, - WorkspaceMemberAdminSerializer, - WorkspaceMemberMeSerializer, - ProjectMemberRoleSerializer, - WorkspaceUserPropertiesSerializer, - WorkspaceEstimateSerializer, - StateSerializer, - LabelSerializer, - CycleSerializer, - ModuleSerializer, -) -from plane.app.views.base import BaseAPIView -from . import BaseViewSet -from plane.db.models import ( - State, - User, - Workspace, - WorkspaceMemberInvite, - Team, - ProjectMember, - IssueActivity, - Issue, - WorkspaceTheme, - IssueLink, - IssueAttachment, - IssueSubscriber, - Project, - Label, - WorkspaceMember, - CycleIssue, - WorkspaceUserProperties, - Estimate, - EstimatePoint, - Module, - ModuleLink, - Cycle, -) -from plane.app.permissions import ( - WorkSpaceBasePermission, - WorkSpaceAdminPermission, - WorkspaceEntityPermission, - WorkspaceViewerPermission, - WorkspaceUserPermission, -) -from plane.bgtasks.workspace_invitation_task import workspace_invitation -from plane.utils.issue_filters import issue_filters -from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.utils.cache import cache_response, invalidate_cache - - -class WorkSpaceViewSet(BaseViewSet): - model = Workspace - serializer_class = WorkSpaceSerializer - permission_classes = [ - WorkSpaceBasePermission, - ] - - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - lookup_field = "slug" - - def get_queryset(self): - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - return ( - self.filter_queryset( - super().get_queryset().select_related("owner") - ) - .order_by("name") - .filter( - workspace_member__member=self.request.user, - workspace_member__is_active=True, - ) - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .select_related("owner") - ) - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def create(self, request): - try: - serializer = WorkSpaceSerializer(data=request.data) - - slug = request.data.get("slug", False) - name = request.data.get("name", False) - - if not name or not slug: - return Response( - {"error": "Both name and slug are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if len(name) > 80 or len(slug) > 48: - return Response( - { - "error": "The maximum length for name is 80 and for slug is 48" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if serializer.is_valid(): - serializer.save(owner=request.user) - # Create Workspace member - _ = WorkspaceMember.objects.create( - workspace_id=serializer.data["id"], - member=request.user, - role=20, - company_role=request.data.get("company_role", ""), - ) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - [serializer.errors[error][0] for error in serializer.errors], - status=status.HTTP_400_BAD_REQUEST, - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"slug": "The workspace with the slug already exists"}, - status=status.HTTP_410_GONE, - ) - - @cache_response(60 * 60 * 2) - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def partial_update(self, request, *args, **kwargs): - return super().partial_update(request, *args, **kwargs) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - -class UserWorkSpacesEndpoint(BaseAPIView): - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - @cache_response(60 * 60 * 2) - def get(self, request): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - workspace = ( - Workspace.objects.prefetch_related( - Prefetch( - "workspace_member", - queryset=WorkspaceMember.objects.filter( - member=request.user, is_active=True - ), - ) - ) - .select_related("owner") - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .filter( - workspace_member__member=request.user, - workspace_member__is_active=True, - ) - .distinct() - ) - workspaces = WorkSpaceSerializer( - self.filter_queryset(workspace), - fields=fields if fields else None, - many=True, - ).data - return Response(workspaces, status=status.HTTP_200_OK) - - -class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): - def get(self, request): - slug = request.GET.get("slug", False) - - if not slug or slug == "": - return Response( - {"error": "Workspace Slug is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.filter(slug=slug).exists() - return Response({"status": not workspace}, status=status.HTTP_200_OK) - - -class WorkspaceInvitationsViewset(BaseViewSet): - """Endpoint for creating, listing and deleting workspaces""" - - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner", "created_by") - ) - - def create(self, request, slug): - emails = request.data.get("emails", []) - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # check for role level of the requesting user - requesting_user = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace object - workspace = Workspace.objects.get(slug=slug) - - # Check if user is already a member of workspace - workspace_members = WorkspaceMember.objects.filter( - workspace_id=workspace.id, - member__email__in=[email.get("email") for email in emails], - is_active=True, - ).select_related("member", "workspace", "workspace__owner") - - if workspace_members: - return Response( - { - "error": "Some users are already member of workspace", - "workspace_users": WorkSpaceMemberSerializer( - workspace_members, many=True - ).data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - workspace_invitations.append( - WorkspaceMemberInvite( - email=email.get("email").strip().lower(), - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Create workspace member invite - workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( - workspace_invitations, batch_size=10, ignore_conflicts=True - ) - - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in workspace_invitations: - workspace_invitation.delay( - invitation.email, - workspace.id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Emails sent successfully", - }, - status=status.HTTP_200_OK, - ) - - def destroy(self, request, slug, pk): - workspace_member_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - workspace_member_invite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - """Invitation response endpoint the user can respond to the invitation""" - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - def post(self, request, slug, pk): - workspace_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - - email = request.data.get("email", "") - - # Check the email - if email == "" or workspace_invite.email != email: - return Response( - {"error": "You do not have permission to join the workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) - - # If already responded then return error - if workspace_invite.responded_at is None: - workspace_invite.accepted = request.data.get("accepted", False) - workspace_invite.responded_at = timezone.now() - workspace_invite.save() - - if workspace_invite.accepted: - # Check if the user created account after invitation - user = User.objects.filter(email=email).first() - - # If the user is present then create the workspace member - if user is not None: - # Check if the user was already a member of workspace then activate the user - workspace_member = WorkspaceMember.objects.filter( - workspace=workspace_invite.workspace, member=user - ).first() - if workspace_member is not None: - workspace_member.is_active = True - workspace_member.role = workspace_invite.role - workspace_member.save() - else: - # Create a Workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_invite.workspace, - member=user, - role=workspace_invite.role, - ) - - # Set the user last_workspace_id to the accepted workspace - user.last_workspace_id = workspace_invite.workspace.id - user.save() - - # Delete the invitation - workspace_invite.delete() - - # Send event - workspace_invite_event.delay( - user=user.id if user is not None else None, - email=email, - user_agent=request.META.get("HTTP_USER_AGENT"), - ip=request.META.get("REMOTE_ADDR"), - event_name="MEMBER_ACCEPTED", - accepted_from="EMAIL", - ) - - return Response( - {"message": "Workspace Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - # Workspace invitation rejected - return Response( - {"message": "Workspace Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, pk): - workspace_invitation = WorkspaceMemberInvite.objects.get( - workspace__slug=slug, pk=pk - ) - serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserWorkspaceInvitationsViewSet(BaseViewSet): - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "created_by") - .annotate(total_members=Count("workspace__workspace_member")) - ) - - @invalidate_cache(path="/api/workspaces/", user=False) - @invalidate_cache(path="/api/users/me/workspaces/") - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def create(self, request): - invitations = request.data.get("invitations", []) - workspace_invitations = WorkspaceMemberInvite.objects.filter( - pk__in=invitations, email=request.user.email - ).order_by("-created_at") - - # If the user is already a member of workspace and was deactivated then activate the user - for invitation in workspace_invitations: - # Update the WorkspaceMember for this specific invitation - WorkspaceMember.objects.filter( - workspace_id=invitation.workspace_id, member=request.user - ).update(is_active=True, role=invitation.role) - - # Bulk create the user for all the workspaces - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - workspace=invitation.workspace, - member=request.user, - role=invitation.role, - created_by=request.user, - ) - for invitation in workspace_invitations - ], - ignore_conflicts=True, - ) - - # Delete joined workspace invites - workspace_invitations.delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkSpaceMemberViewSet(BaseViewSet): - serializer_class = WorkspaceMemberAdminSerializer - model = WorkspaceMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - WorkspaceUserPermission, - ] - else: - self.permission_classes = [ - WorkspaceEntityPermission, - ] - - return super(WorkSpaceMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - .select_related("workspace", "workspace__owner") - .select_related("member") - ) - - @cache_response(60 * 60 * 2) - def list(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - # Get all active workspace members - workspace_members = self.get_queryset() - - if workspace_member.role > 10: - serializer = WorkspaceMemberAdminSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - else: - serializer = WorkSpaceMemberSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def partial_update(self, request, slug, pk): - workspace_member = WorkspaceMember.objects.get( - pk=pk, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ) - if request.user.id == workspace_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the requested user role - requested_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - # Check if role is being updated - # One cannot update role higher than his own role - if ( - "role" in request.data - and int(request.data.get("role", workspace_member.role)) - > requested_workspace_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = WorkSpaceMemberSerializer( - workspace_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def destroy(self, request, slug, pk): - # Check the user role who is deleting the user - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - pk=pk, - member__is_bot=False, - is_active=True, - ) - - # check requesting user role - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - if str(workspace_member.id) == str(requesting_workspace_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if requesting_workspace_member.role < workspace_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=workspace_member.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - @invalidate_cache(path="/api/workspaces/:slug/members/", url_params=True, user=False) - def leave(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the workspace - if ( - workspace_member.role == 20 - and not WorkspaceMember.objects.filter( - workspace__slug=slug, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=request.user.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - # # Deactivate the user - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceProjectMemberEndpoint(BaseAPIView): - serializer_class = ProjectMemberRoleSerializer - model = ProjectMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug): - # Fetch all project IDs where the user is involved - project_ids = ( - ProjectMember.objects.filter( - member=request.user, - is_active=True, - ) - .values_list("project_id", flat=True) - .distinct() - ) - - # Get all the project members in which the user is involved - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - is_active=True, - ).select_related("project", "member", "workspace") - project_members = ProjectMemberRoleSerializer( - project_members, many=True - ).data - - project_members_dict = dict() - - # Construct a dictionary with project_id as key and project_members as value - for project_member in project_members: - project_id = project_member.pop("project") - if str(project_id) not in project_members_dict: - project_members_dict[str(project_id)] = [] - project_members_dict[str(project_id)].append(project_member) - - return Response(project_members_dict, status=status.HTTP_200_OK) - - -class TeamMemberViewSet(BaseViewSet): - serializer_class = TeamSerializer - model = Team - permission_classes = [ - WorkSpaceAdminPermission, - ] - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") - .prefetch_related("members") - ) - - def create(self, request, slug): - members = list( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member__id__in=request.data.get("members", []), - is_active=True, - ) - .annotate(member_str_id=Cast("member", output_field=CharField())) - .distinct() - .values_list("member_str_id", flat=True) - ) - - if len(members) != len(request.data.get("members", [])): - users = list( - set(request.data.get("members", [])).difference(members) - ) - users = User.objects.filter(pk__in=users) - - serializer = UserLiteSerializer(users, many=True) - return Response( - { - "error": f"{len(users)} of the member(s) are not a part of the workspace", - "members": serializer.data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - serializer = TeamSerializer( - data=request.data, context={"workspace": workspace} - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): - def get(self, request): - user = User.objects.get(pk=request.user.id) - - last_workspace_id = user.last_workspace_id - - if last_workspace_id is None: - return Response( - { - "project_details": [], - "workspace_details": {}, - }, - status=status.HTTP_200_OK, - ) - - workspace = Workspace.objects.get(pk=last_workspace_id) - workspace_serializer = WorkSpaceSerializer(workspace) - - project_member = ProjectMember.objects.filter( - workspace_id=last_workspace_id, member=request.user - ).select_related("workspace", "project", "member", "workspace__owner") - - project_member_serializer = ProjectMemberSerializer( - project_member, many=True - ) - - return Response( - { - "workspace_details": workspace_serializer.data, - "project_details": project_member_serializer.data, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceMemberUserEndpoint(BaseAPIView): - def get(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - serializer = WorkspaceMemberMeSerializer(workspace_member) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceMemberUserViewsEndpoint(BaseAPIView): - def post(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - workspace_member.view_props = request.data.get("view_props", {}) - workspace_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class UserActivityGraphEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-6), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - return Response(issue_activities, status=status.HTTP_200_OK) - - -class UserIssueCompletedGraphEndpoint(BaseAPIView): - def get(self, request, slug): - month = request.GET.get("month", 1) - - issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(completed_week=ExtractWeek("completed_at")) - .annotate(week=F("completed_week") % 4) - .values("week") - .annotate(completed_count=Count("completed_week")) - .order_by("week") - ) - - return Response(issues, status=status.HTTP_200_OK) - - -class WeekInMonth(Func): - function = "FLOOR" - template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" - - -class UserWorkspaceDashboardEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-3), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - month = request.GET.get("month", 1) - - completed_issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(day_of_month=ExtractDay("completed_at")) - .annotate(week_in_month=WeekInMonth(F("day_of_month"))) - .values("week_in_month") - .annotate(completed_count=Count("id")) - .order_by("week_in_month") - ) - - assigned_issues = Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ).count() - - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - ).count() - - completed_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - state__group="completed", - ).count() - - issues_due_week = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - ) - .annotate(target_week=ExtractWeek("target_date")) - .filter(target_week=timezone.now().date().isocalendar()[1]) - .count() - ) - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - overdue_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - target_date__lt=timezone.now(), - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "target_date") - - upcoming_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - start_date__gte=timezone.now(), - workspace__slug=slug, - assignees__in=[request.user], - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "start_date") - - return Response( - { - "issue_activities": issue_activities, - "completed_issues": completed_issues, - "assigned_issues_count": assigned_issues, - "pending_issues_count": pending_issues_count, - "completed_issues_count": completed_issues_count, - "issues_due_week_count": issues_due_week, - "state_distribution": state_distribution, - "overdue_issues": overdue_issues, - "upcoming_issues": upcoming_issues, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceThemeViewSet(BaseViewSet): - permission_classes = [ - WorkSpaceAdminPermission, - ] - model = WorkspaceTheme - serializer_class = WorkspaceThemeSerializer - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - ) - - def create(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - serializer = WorkspaceThemeSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(workspace=workspace, actor=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class WorkspaceUserProfileStatsEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - filters = issue_filters(request.query_params, "GET") - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - priority_order = ["urgent", "high", "medium", "low", "none"] - - priority_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .values("priority") - .annotate(priority_count=Count("priority")) - .filter(priority_count__gte=1) - .annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - default=Value(len(priority_order)), - output_field=IntegerField(), - ) - ) - .order_by("priority_order") - ) - - created_issues = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - created_by_id=user_id, - ) - .filter(**filters) - .count() - ) - - assigned_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - pending_issues_count = ( - Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - completed_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - state__group="completed", - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - subscribed_issues_count = ( - IssueSubscriber.objects.filter( - workspace__slug=slug, - subscriber_id=user_id, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - upcoming_cycles = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - present_cycle = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__lt=timezone.now().date(), - cycle__end_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - return Response( - { - "state_distribution": state_distribution, - "priority_distribution": priority_distribution, - "created_issues": created_issues, - "assigned_issues": assigned_issues_count, - "completed_issues": completed_issues_count, - "pending_issues": pending_issues_count, - "subscribed_issues": subscribed_issues_count, - "present_cycles": present_cycle, - "upcoming_cycles": upcoming_cycles, - } - ) - - -class WorkspaceUserActivityEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug, user_id): - projects = request.query_params.getlist("project", []) - - queryset = IssueActivity.objects.filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - actor=user_id, - ).select_related("actor", "workspace", "issue", "project") - - if projects: - queryset = queryset.filter(project__in=projects) - - return self.paginate( - request=request, - queryset=queryset, - on_results=lambda issue_activities: IssueActivitySerializer( - issue_activities, many=True - ).data, - ) - - -class ExportWorkspaceUserActivityEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def generate_csv_from_rows(self, rows): - """Generate CSV buffer from rows.""" - csv_buffer = io.StringIO() - writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) - [writer.writerow(row) for row in rows] - csv_buffer.seek(0) - return csv_buffer - - def post(self, request, slug, user_id): - - if not request.data.get("date"): - return Response( - {"error": "Date is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - user_activities = IssueActivity.objects.filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - workspace__slug=slug, - created_at__date=request.data.get("date"), - project__project_projectmember__member=request.user, - actor_id=user_id, - ).select_related("actor", "workspace", "issue", "project")[:10000] - - header = [ - "Actor name", - "Issue ID", - "Project", - "Created at", - "Updated at", - "Action", - "Field", - "Old value", - "New value", - ] - rows = [ - ( - activity.actor.display_name, - f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", - activity.project.name, - activity.created_at, - activity.updated_at, - activity.verb, - activity.field, - activity.old_value, - activity.new_value, - ) - for activity in user_activities - ] - csv_buffer = self.generate_csv_from_rows([header] + rows) - response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"' - return response - - -class WorkspaceUserProfileEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - user_data = User.objects.get(pk=user_id) - - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - projects = [] - if requesting_workspace_member.role >= 10: - projects = ( - Project.objects.filter( - workspace__slug=slug, - project_projectmember__member=request.user, - project_projectmember__is_active=True, - ) - .annotate( - created_issues=Count( - "project_issue", - filter=Q( - project_issue__created_by_id=user_id, - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - assigned_issues=Count( - "project_issue", - filter=Q( - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "project_issue", - filter=Q( - project_issue__completed_at__isnull=False, - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "project_issue", - filter=Q( - project_issue__state__group__in=[ - "backlog", - "unstarted", - "started", - ], - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .values( - "id", - "created_issues", - "assigned_issues", - "completed_issues", - "pending_issues", - ) - ) - - return Response( - { - "project_data": projects, - "user_data": { - "email": user_data.email, - "first_name": user_data.first_name, - "last_name": user_data.last_name, - "avatar": user_data.avatar, - "cover_image": user_data.cover_image, - "date_joined": user_data.date_joined, - "user_timezone": user_data.user_timezone, - "display_name": user_data.display_name, - }, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug, user_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - Issue.issue_objects.filter( - Q(assignees__in=[user_id]) - | Q(created_by_id=user_id) - | Q(issue_subscribers__subscriber_id=user_id), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .order_by("created_at") - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" - if order_by_param.startswith("-") - else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data - return Response(issues, status=status.HTTP_200_OK) - - -class WorkspaceLabelsEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - labels = Label.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - serializer = LabelSerializer(labels, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceStatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - states = State.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - serializer = StateSerializer(states, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceEstimatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - @cache_response(60 * 60 * 2) - def get(self, request, slug): - estimate_ids = Project.objects.filter( - workspace__slug=slug, estimate__isnull=False - ).values_list("estimate_id", flat=True) - estimates = Estimate.objects.filter( - pk__in=estimate_ids - ).prefetch_related( - Prefetch( - "points", - queryset=EstimatePoint.objects.select_related( - "estimate", "workspace", "project" - ), - ) - ) - serializer = WorkspaceEstimateSerializer(estimates, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceModulesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - modules = ( - Module.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("lead") - .prefetch_related("members") - .prefetch_related( - Prefetch( - "link_module", - queryset=ModuleLink.objects.select_related( - "module", "created_by" - ), - ) - ) - .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="completed", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="cancelled", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="started", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="unstarted", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="backlog", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - ) - - serializer = ModuleSerializer(modules, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceCyclesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - cycles = ( - Cycle.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - total_estimates=Sum("issue_cycle__issue__estimate_point") - ) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - .distinct() - ) - serializer = CycleSerializer(cycles, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceUserPropertiesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def patch(self, request, slug): - workspace_properties = WorkspaceUserProperties.objects.get( - user=request.user, - workspace__slug=slug, - ) - - workspace_properties.filters = request.data.get( - "filters", workspace_properties.filters - ) - workspace_properties.display_filters = request.data.get( - "display_filters", workspace_properties.display_filters - ) - workspace_properties.display_properties = request.data.get( - "display_properties", workspace_properties.display_properties - ) - workspace_properties.save() - - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug): - ( - workspace_properties, - _, - ) = WorkspaceUserProperties.objects.get_or_create( - user=request.user, workspace__slug=slug - ) - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py new file mode 100644 index 00000000000..0fb8f2d8037 --- /dev/null +++ b/apiserver/plane/app/views/workspace/base.py @@ -0,0 +1,414 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta +import csv +import io + + +# Django imports +from django.http import HttpResponse +from django.db import IntegrityError +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, +) +from django.db.models.functions import ExtractWeek, Cast, ExtractDay +from django.db.models.fields import DateField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + WorkspaceThemeSerializer, +) +from plane.app.views.base import BaseViewSet, BaseAPIView +from plane.db.models import ( + Workspace, + IssueActivity, + Issue, + WorkspaceTheme, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceBasePermission, + WorkSpaceAdminPermission, + WorkspaceEntityPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + +class WorkSpaceViewSet(BaseViewSet): + model = Workspace + serializer_class = WorkSpaceSerializer + permission_classes = [ + WorkSpaceBasePermission, + ] + + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + lookup_field = "slug" + + def get_queryset(self): + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + return ( + self.filter_queryset( + super().get_queryset().select_related("owner") + ) + .order_by("name") + .filter( + workspace_member__member=self.request.user, + workspace_member__is_active=True, + ) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .select_related("owner") + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def create(self, request): + try: + serializer = WorkSpaceSerializer(data=request.data) + + slug = request.data.get("slug", False) + name = request.data.get("name", False) + + if not name or not slug: + return Response( + {"error": "Both name and slug are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if len(name) > 80 or len(slug) > 48: + return Response( + { + "error": "The maximum length for name is 80 and for slug is 48" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if serializer.is_valid(): + serializer.save(owner=request.user) + # Create Workspace member + _ = WorkspaceMember.objects.create( + workspace_id=serializer.data["id"], + member=request.user, + role=20, + company_role=request.data.get("company_role", ""), + ) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + [serializer.errors[error][0] for error in serializer.errors], + status=status.HTTP_400_BAD_REQUEST, + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"slug": "The workspace with the slug already exists"}, + status=status.HTTP_410_GONE, + ) + @cache_response(60 * 60 * 2) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class UserWorkSpacesEndpoint(BaseAPIView): + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + @cache_response(60 * 60 * 2) + def get(self, request): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + workspace = ( + Workspace.objects.prefetch_related( + Prefetch( + "workspace_member", + queryset=WorkspaceMember.objects.filter( + member=request.user, is_active=True + ), + ) + ) + .select_related("owner") + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .filter( + workspace_member__member=request.user, + workspace_member__is_active=True, + ) + .distinct() + ) + workspaces = WorkSpaceSerializer( + self.filter_queryset(workspace), + fields=fields if fields else None, + many=True, + ).data + return Response(workspaces, status=status.HTTP_200_OK) + + +class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): + def get(self, request): + slug = request.GET.get("slug", False) + + if not slug or slug == "": + return Response( + {"error": "Workspace Slug is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.filter(slug=slug).exists() + return Response({"status": not workspace}, status=status.HTTP_200_OK) + + +class WeekInMonth(Func): + function = "FLOOR" + template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" + + +class UserWorkspaceDashboardEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-3), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + month = request.GET.get("month", 1) + + completed_issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(day_of_month=ExtractDay("completed_at")) + .annotate(week_in_month=WeekInMonth(F("day_of_month"))) + .values("week_in_month") + .annotate(completed_count=Count("id")) + .order_by("week_in_month") + ) + + assigned_issues = Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ).count() + + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + ).count() + + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + state__group="completed", + ).count() + + issues_due_week = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + ) + .annotate(target_week=ExtractWeek("target_date")) + .filter(target_week=timezone.now().date().isocalendar()[1]) + .count() + ) + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + overdue_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + target_date__lt=timezone.now(), + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "target_date") + + upcoming_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + start_date__gte=timezone.now(), + workspace__slug=slug, + assignees__in=[request.user], + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "start_date") + + return Response( + { + "issue_activities": issue_activities, + "completed_issues": completed_issues, + "assigned_issues_count": assigned_issues, + "pending_issues_count": pending_issues_count, + "completed_issues_count": completed_issues_count, + "issues_due_week_count": issues_due_week, + "state_distribution": state_distribution, + "overdue_issues": overdue_issues, + "upcoming_issues": upcoming_issues, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceThemeViewSet(BaseViewSet): + permission_classes = [ + WorkSpaceAdminPermission, + ] + model = WorkspaceTheme + serializer_class = WorkspaceThemeSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + ) + + def create(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + serializer = WorkspaceThemeSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(workspace=workspace, actor=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ExportWorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def generate_csv_from_rows(self, rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + csv_buffer.seek(0) + return csv_buffer + + def post(self, request, slug, user_id): + + if not request.data.get("date"): + return Response( + {"error": "Date is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user_activities = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + created_at__date=request.data.get("date"), + project__project_projectmember__member=request.user, + actor_id=user_id, + ).select_related("actor", "workspace", "issue", "project")[:10000] + + header = [ + "Actor name", + "Issue ID", + "Project", + "Created at", + "Updated at", + "Action", + "Field", + "Old value", + "New value", + ] + rows = [ + ( + activity.actor.display_name, + f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", + activity.project.name, + activity.created_at, + activity.updated_at, + activity.verb, + activity.field, + activity.old_value, + activity.new_value, + ) + for activity in user_activities + ] + csv_buffer = self.generate_csv_from_rows([header] + rows) + response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") + response["Content-Disposition"] = ( + 'attachment; filename="workspace-user-activity.csv"' + ) + return response diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py new file mode 100644 index 00000000000..ea081cf9930 --- /dev/null +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -0,0 +1,116 @@ +# Django imports +from django.db.models import ( + Q, + Count, + Sum, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import Cycle +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.cycle import CycleSerializer + + +class WorkspaceCyclesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + cycles = ( + Cycle.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("owned_by") + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + total_estimates=Sum("issue_cycle__issue__estimate_point") + ) + .annotate( + completed_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + .distinct() + ) + serializer = CycleSerializer(cycles, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/estimate.py b/apiserver/plane/app/views/workspace/estimate.py new file mode 100644 index 00000000000..6b64d8c908e --- /dev/null +++ b/apiserver/plane/app/views/workspace/estimate.py @@ -0,0 +1,39 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import WorkspaceEstimateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Project, Estimate +from plane.app.permissions import WorkspaceEntityPermission + +# Django imports +from django.db.models import ( + Prefetch, +) +from plane.utils.cache import cache_response + + +class WorkspaceEstimatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + estimate_ids = Project.objects.filter( + workspace__slug=slug, estimate__isnull=False + ).values_list("estimate_id", flat=True) + estimates = Estimate.objects.filter( + pk__in=estimate_ids + ).prefetch_related( + Prefetch( + "points", + queryset=Project.objects.select_related( + "estimate", "workspace", "project" + ), + ) + ) + serializer = WorkspaceEstimateSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py new file mode 100644 index 00000000000..807c060ad21 --- /dev/null +++ b/apiserver/plane/app/views/workspace/invite.py @@ -0,0 +1,301 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.conf import settings +from django.utils import timezone +from django.db.models import Count +from django.core.exceptions import ValidationError +from django.core.validators import validate_email + +# Third party modules +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + WorkSpaceMemberInviteSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + WorkspaceMemberInvite, + WorkspaceMember, +) +from plane.app.permissions import WorkSpaceAdminPermission +from plane.bgtasks.workspace_invitation_task import workspace_invitation +from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.utils.cache import invalidate_cache + +class WorkspaceInvitationsViewset(BaseViewSet): + """Endpoint for creating, listing and deleting workspaces""" + + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + permission_classes = [ + WorkSpaceAdminPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner", "created_by") + ) + + def create(self, request, slug): + emails = request.data.get("emails", []) + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # check for role level of the requesting user + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the workspace object + workspace = Workspace.objects.get(slug=slug) + + # Check if user is already a member of workspace + workspace_members = WorkspaceMember.objects.filter( + workspace_id=workspace.id, + member__email__in=[email.get("email") for email in emails], + is_active=True, + ).select_related("member", "workspace", "workspace__owner") + + if workspace_members: + return Response( + { + "error": "Some users are already member of workspace", + "workspace_users": WorkSpaceMemberSerializer( + workspace_members, many=True + ).data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + workspace_invitations.append( + WorkspaceMemberInvite( + email=email.get("email").strip().lower(), + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Create workspace member invite + workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( + workspace_invitations, batch_size=10, ignore_conflicts=True + ) + + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in workspace_invitations: + workspace_invitation.delay( + invitation.email, + workspace.id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Emails sent successfully", + }, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, pk): + workspace_member_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + workspace_member_invite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + """Invitation response endpoint the user can respond to the invitation""" + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def post(self, request, slug, pk): + workspace_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + + email = request.data.get("email", "") + + # Check the email + if email == "" or workspace_invite.email != email: + return Response( + {"error": "You do not have permission to join the workspace"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # If already responded then return error + if workspace_invite.responded_at is None: + workspace_invite.accepted = request.data.get("accepted", False) + workspace_invite.responded_at = timezone.now() + workspace_invite.save() + + if workspace_invite.accepted: + # Check if the user created account after invitation + user = User.objects.filter(email=email).first() + + # If the user is present then create the workspace member + if user is not None: + # Check if the user was already a member of workspace then activate the user + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace_invite.workspace, member=user + ).first() + if workspace_member is not None: + workspace_member.is_active = True + workspace_member.role = workspace_invite.role + workspace_member.save() + else: + # Create a Workspace + _ = WorkspaceMember.objects.create( + workspace=workspace_invite.workspace, + member=user, + role=workspace_invite.role, + ) + + # Set the user last_workspace_id to the accepted workspace + user.last_workspace_id = workspace_invite.workspace.id + user.save() + + # Delete the invitation + workspace_invite.delete() + + # Send event + workspace_invite_event.delay( + user=user.id if user is not None else None, + email=email, + user_agent=request.META.get("HTTP_USER_AGENT"), + ip=request.META.get("REMOTE_ADDR"), + event_name="MEMBER_ACCEPTED", + accepted_from="EMAIL", + ) + + return Response( + {"message": "Workspace Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + # Workspace invitation rejected + return Response( + {"message": "Workspace Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, pk): + workspace_invitation = WorkspaceMemberInvite.objects.get( + workspace__slug=slug, pk=pk + ) + serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserWorkspaceInvitationsViewSet(BaseViewSet): + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "created_by") + .annotate(total_members=Count("workspace__workspace_member")) + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def create(self, request): + invitations = request.data.get("invitations", []) + workspace_invitations = WorkspaceMemberInvite.objects.filter( + pk__in=invitations, email=request.user.email + ).order_by("-created_at") + + # If the user is already a member of workspace and was deactivated then activate the user + for invitation in workspace_invitations: + # Update the WorkspaceMember for this specific invitation + WorkspaceMember.objects.filter( + workspace_id=invitation.workspace_id, member=request.user + ).update(is_active=True, role=invitation.role) + + # Bulk create the user for all the workspaces + WorkspaceMember.objects.bulk_create( + [ + WorkspaceMember( + workspace=invitation.workspace, + member=request.user, + role=invitation.role, + created_by=request.user, + ) + for invitation in workspace_invitations + ], + ignore_conflicts=True, + ) + + # Delete joined workspace invites + workspace_invitations.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/label.py b/apiserver/plane/app/views/workspace/label.py new file mode 100644 index 00000000000..ba396a8429d --- /dev/null +++ b/apiserver/plane/app/views/workspace/label.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import LabelSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Label +from plane.app.permissions import WorkspaceViewerPermission +from plane.utils.cache import cache_response + +class WorkspaceLabelsEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + labels = Label.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = LabelSerializer(labels, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py new file mode 100644 index 00000000000..ff88e47f8ab --- /dev/null +++ b/apiserver/plane/app/views/workspace/member.py @@ -0,0 +1,396 @@ +# Django imports +from django.db.models import ( + Q, + Count, +) +from django.db.models.functions import Cast +from django.db.models import CharField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + TeamSerializer, + UserLiteSerializer, + WorkspaceMemberAdminSerializer, + WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + Team, + ProjectMember, + Project, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceAdminPermission, + WorkspaceEntityPermission, + WorkspaceUserPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + + +class WorkSpaceMemberViewSet(BaseViewSet): + serializer_class = WorkspaceMemberAdminSerializer + model = WorkspaceMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + WorkspaceUserPermission, + ] + else: + self.permission_classes = [ + WorkspaceEntityPermission, + ] + + return super(WorkSpaceMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + .select_related("workspace", "workspace__owner") + .select_related("member") + ) + + @cache_response(60 * 60 * 2) + def list(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + # Get all active workspace members + workspace_members = self.get_queryset() + + if workspace_member.role > 10: + serializer = WorkspaceMemberAdminSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + else: + serializer = WorkSpaceMemberSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def partial_update(self, request, slug, pk): + workspace_member = WorkspaceMember.objects.get( + pk=pk, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ) + if request.user.id == workspace_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + # Check if role is being updated + # One cannot update role higher than his own role + if ( + "role" in request.data + and int(request.data.get("role", workspace_member.role)) + > requested_workspace_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = WorkSpaceMemberSerializer( + workspace_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def destroy(self, request, slug, pk): + # Check the user role who is deleting the user + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + pk=pk, + member__is_bot=False, + is_active=True, + ) + + # check requesting user role + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + if str(workspace_member.id) == str(requesting_workspace_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if requesting_workspace_member.role < workspace_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=workspace_member.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def leave(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the workspace + if ( + workspace_member.role == 20 + and not WorkspaceMember.objects.filter( + workspace__slug=slug, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=request.user.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + # # Deactivate the user + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserViewsEndpoint(BaseAPIView): + def post(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + workspace_member.view_props = request.data.get("view_props", {}) + workspace_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserEndpoint(BaseAPIView): + def get(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + serializer = WorkspaceMemberMeSerializer(workspace_member) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceProjectMemberEndpoint(BaseAPIView): + serializer_class = ProjectMemberRoleSerializer + model = ProjectMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + # Fetch all project IDs where the user is involved + project_ids = ( + ProjectMember.objects.filter( + member=request.user, + is_active=True, + ) + .values_list("project_id", flat=True) + .distinct() + ) + + # Get all the project members in which the user is involved + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + is_active=True, + ).select_related("project", "member", "workspace") + project_members = ProjectMemberRoleSerializer( + project_members, many=True + ).data + + project_members_dict = dict() + + # Construct a dictionary with project_id as key and project_members as value + for project_member in project_members: + project_id = project_member.pop("project") + if str(project_id) not in project_members_dict: + project_members_dict[str(project_id)] = [] + project_members_dict[str(project_id)].append(project_member) + + return Response(project_members_dict, status=status.HTTP_200_OK) + + +class TeamMemberViewSet(BaseViewSet): + serializer_class = TeamSerializer + model = Team + permission_classes = [ + WorkSpaceAdminPermission, + ] + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner") + .prefetch_related("members") + ) + + def create(self, request, slug): + members = list( + WorkspaceMember.objects.filter( + workspace__slug=slug, + member__id__in=request.data.get("members", []), + is_active=True, + ) + .annotate(member_str_id=Cast("member", output_field=CharField())) + .distinct() + .values_list("member_str_id", flat=True) + ) + + if len(members) != len(request.data.get("members", [])): + users = list( + set(request.data.get("members", [])).difference(members) + ) + users = User.objects.filter(pk__in=users) + + serializer = UserLiteSerializer(users, many=True) + return Response( + { + "error": f"{len(users)} of the member(s) are not a part of the workspace", + "members": serializer.data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + serializer = TeamSerializer( + data=request.data, context={"workspace": workspace} + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/views/workspace/module.py b/apiserver/plane/app/views/workspace/module.py new file mode 100644 index 00000000000..fbd760271a6 --- /dev/null +++ b/apiserver/plane/app/views/workspace/module.py @@ -0,0 +1,104 @@ +# Django imports +from django.db.models import ( + Prefetch, + Q, + Count, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + Module, + ModuleLink, +) +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.module import ModuleSerializer + +class WorkspaceModulesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + modules = ( + Module.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("lead") + .prefetch_related("members") + .prefetch_related( + Prefetch( + "link_module", + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), + ) + ) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) + .annotate( + completed_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="completed", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="cancelled", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="started", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="unstarted", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="backlog", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + ) + + serializer = ModuleSerializer(modules, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/state.py b/apiserver/plane/app/views/workspace/state.py new file mode 100644 index 00000000000..d44f83e73e8 --- /dev/null +++ b/apiserver/plane/app/views/workspace/state.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import StateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import State +from plane.app.permissions import WorkspaceEntityPermission +from plane.utils.cache import cache_response + +class WorkspaceStatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + states = State.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = StateSerializer(states, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py new file mode 100644 index 00000000000..36b00b73832 --- /dev/null +++ b/apiserver/plane/app/views/workspace/user.py @@ -0,0 +1,573 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, + Max, + IntegerField, + UUIDField, +) +from django.db.models.functions import ExtractWeek, Cast +from django.db.models.fields import DateField +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + ProjectMemberSerializer, + IssueActivitySerializer, + IssueSerializer, + WorkspaceUserPropertiesSerializer, +) +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + User, + Workspace, + ProjectMember, + IssueActivity, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + Project, + WorkspaceMember, + CycleIssue, + WorkspaceUserProperties, +) +from plane.app.permissions import ( + WorkspaceEntityPermission, + WorkspaceViewerPermission, +) +from plane.utils.issue_filters import issue_filters + + +class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): + def get(self, request): + user = User.objects.get(pk=request.user.id) + + last_workspace_id = user.last_workspace_id + + if last_workspace_id is None: + return Response( + { + "project_details": [], + "workspace_details": {}, + }, + status=status.HTTP_200_OK, + ) + + workspace = Workspace.objects.get(pk=last_workspace_id) + workspace_serializer = WorkSpaceSerializer(workspace) + + project_member = ProjectMember.objects.filter( + workspace_id=last_workspace_id, member=request.user + ).select_related("workspace", "project", "member", "workspace__owner") + + project_member_serializer = ProjectMemberSerializer( + project_member, many=True + ) + + return Response( + { + "workspace_details": workspace_serializer.data, + "project_details": project_member_serializer.data, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug, user_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( + Issue.issue_objects.filter( + Q(assignees__in=[user_id]) + | Q(created_by_id=user_id) + | Q(issue_subscribers__subscriber_id=user_id), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True + ) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by("created_at") + ).distinct() + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) + + +class WorkspaceUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def patch(self, request, slug): + workspace_properties = WorkspaceUserProperties.objects.get( + user=request.user, + workspace__slug=slug, + ) + + workspace_properties.filters = request.data.get( + "filters", workspace_properties.filters + ) + workspace_properties.display_filters = request.data.get( + "display_filters", workspace_properties.display_filters + ) + workspace_properties.display_properties = request.data.get( + "display_properties", workspace_properties.display_properties + ) + workspace_properties.save() + + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug): + ( + workspace_properties, + _, + ) = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace__slug=slug + ) + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceUserProfileEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + user_data = User.objects.get(pk=user_id) + + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + projects = [] + if requesting_workspace_member.role >= 10: + projects = ( + Project.objects.filter( + workspace__slug=slug, + project_projectmember__member=request.user, + project_projectmember__is_active=True, + ) + .annotate( + created_issues=Count( + "project_issue", + filter=Q( + project_issue__created_by_id=user_id, + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q( + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "project_issue", + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ], + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .values( + "id", + "logo_props", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", + ) + ) + + return Response( + { + "project_data": projects, + "user_data": { + "email": user_data.email, + "first_name": user_data.first_name, + "last_name": user_data.last_name, + "avatar": user_data.avatar, + "cover_image": user_data.cover_image, + "date_joined": user_data.date_joined, + "user_timezone": user_data.user_timezone, + "display_name": user_data.display_name, + }, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + projects = request.query_params.getlist("project", []) + + queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + actor=user_id, + ).select_related("actor", "workspace", "issue", "project") + + if projects: + queryset = queryset.filter(project__in=projects) + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) + + +class WorkspaceUserProfileStatsEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + filters = issue_filters(request.query_params, "GET") + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + priority_order = ["urgent", "high", "medium", "low", "none"] + + priority_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .values("priority") + .annotate(priority_count=Count("priority")) + .filter(priority_count__gte=1) + .annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + default=Value(len(priority_order)), + output_field=IntegerField(), + ) + ) + .order_by("priority_order") + ) + + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + created_by_id=user_id, + ) + .filter(**filters) + .count() + ) + + assigned_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + completed_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + subscribed_issues_count = ( + IssueSubscriber.objects.filter( + workspace__slug=slug, + subscriber_id=user_id, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + upcoming_cycles = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + present_cycle = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + return Response( + { + "state_distribution": state_distribution, + "priority_distribution": priority_distribution, + "created_issues": created_issues, + "assigned_issues": assigned_issues_count, + "completed_issues": completed_issues_count, + "pending_issues": pending_issues_count, + "subscribed_issues": subscribed_issues_count, + "present_cycles": present_cycle, + "upcoming_cycles": upcoming_cycles, + } + ) + + +class UserActivityGraphEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-6), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + return Response(issue_activities, status=status.HTTP_200_OK) + + +class UserIssueCompletedGraphEndpoint(BaseAPIView): + def get(self, request, slug): + month = request.GET.get("month", 1) + + issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(completed_week=ExtractWeek("completed_at")) + .annotate(week=F("completed_week") % 4) + .values("week") + .annotate(completed_count=Count("completed_week")) + .order_by("week") + ) + + return Response(issues, status=status.HTTP_200_OK) From 7a8aef45993df4c6de60e34c98eb981da7c5befb Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 7 Mar 2024 15:56:02 +0530 Subject: [PATCH 041/214] Fix: rendering issue in kanban swimlanes when the cycle or module is not assigned to an issue (#3899) --- web/store/issue/helpers/issue-helper.store.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 50e04e890b2..cc86560bd91 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -62,35 +62,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") { // 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 || {})?.[_issue?.state_id]?.group || "None"; + 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 +99,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?.stateMap || {})?.[_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 +217,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": @@ -388,7 +390,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()]; From 47a7f60611239f0b58b8b340d7ba6af15b97aaf4 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:43 +0530 Subject: [PATCH 042/214] [WEB-667] fix: unable to deselect project lead in create project modal (#3898) * fix: unable to deselect project lead in create project modal * removed unneccessary code --- web/components/project/create-project-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 4d30c3decac..b8e59f032f1 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -406,7 +406,7 @@ export const CreateProjectModal: FC = observer((props) => {
onChange(lead === value ? null : lead)} placeholder="Lead" multiple={false} buttonVariant="border-with-text" From 7b88a2a88cc777be716a3afdd80bc82ea791bad5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:59 +0530 Subject: [PATCH 043/214] [WEB-469] chore: selected filter sorting added in filter dropdown (#3869) * chore: selected filter sorting added in filter dropdown * chore: handleClearAllFilters function updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated --------- Co-authored-by: gurusainath --- .../empty-states/archived-issues.tsx | 2 +- .../empty-states/draft-issues.tsx | 2 +- .../empty-states/project-issues.tsx | 2 +- .../applied-filters/roots/archived-issue.tsx | 2 +- .../applied-filters/roots/cycle-root.tsx | 2 +- .../applied-filters/roots/draft-issue.tsx | 2 +- .../roots/global-view-root.tsx | 2 +- .../applied-filters/roots/module-root.tsx | 2 +- .../roots/profile-issues-root.tsx | 2 +- .../applied-filters/roots/project-root.tsx | 2 +- .../roots/project-view-root.tsx | 2 +- .../filters/header/filters/assignee.tsx | 37 ++++++++++++------- .../filters/header/filters/created-by.tsx | 34 +++++++++++------ .../filters/header/filters/cycle.tsx | 34 ++++++++++------- .../filters/header/filters/labels.tsx | 31 +++++++++++----- .../filters/header/filters/mentions.tsx | 33 +++++++++++------ .../filters/header/filters/module.tsx | 34 ++++++++++------- .../filters/header/filters/project.tsx | 31 +++++++++++----- .../filters/header/filters/state-group.tsx | 2 +- .../filters/header/filters/state.tsx | 26 ++++++++----- .../issue-layouts/roots/cycle-layout-root.tsx | 2 +- .../roots/module-layout-root.tsx | 2 +- 22 files changed, 180 insertions(+), 108 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index c9de2279c89..36c895de301 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -32,7 +32,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 0968ed07afe..c23fea100b7 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -31,7 +31,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index 12642d364d6..58929e48d00 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -34,7 +34,7 @@ export const ProjectEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 35651d8701a..7e6926fa5f5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -56,7 +56,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 6a741b73d7b..ee6b4e694d4 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -62,7 +62,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index a075d59d2ec..61c0e346aad 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -53,7 +53,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index a431652f14b..d907cf1682c 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -76,7 +76,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { if (!workspaceSlug || !globalViewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index b49ddf4d6bc..fc78d79ea87 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -61,7 +61,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 91eeef423c1..b0c496a7bde 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -57,7 +57,7 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !userId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString()); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index c0b67043a72..602f3fa2d80 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -59,7 +59,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 760d2e7e4ba..6586159fa01 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -67,7 +67,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !viewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index b26b688afc7..c51fcf7ab77 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks -import { Avatar, Loader } from "@plane/ui"; -import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components +import { FilterHeader, FilterOption } from "components/issues"; // ui +import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,15 +25,23 @@ export const FilterAssignees: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 45e3309a9bd..765955bf918 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Avatar, Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,16 +23,25 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + const appliedFiltersCount = appliedFilters?.length ?? 0; const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -43,10 +53,10 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -60,13 +70,13 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx index 396addde68e..b3a65a399a1 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -31,16 +31,24 @@ export const FilterCycle: React.FC = observer((props) => { const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined; const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())), - (cycle) => cycle.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (cycles || []).filter((cycle) => + cycle.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (cycle) => !appliedFilters?.includes(cycle.id), + (cycle) => cycle.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; @@ -54,10 +62,10 @@ export const FilterCycle: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { activePulse={cycleStatus(cycle?.status) === "current" ? true : false} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index 42e95553571..7097b133715 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterLabels: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (labels || []).filter((label) => + label.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (label) => !(appliedFilters ?? []).includes(label.id), + (label) => label.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterLabels: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((label) => ( + {sortedOptions.slice(0, itemsToRender).map((label) => ( = observer((props) => { title={label.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index 4d2839b2c86..80c16478ad9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Loader, Avatar } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -24,15 +25,23 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx index 812cf939f22..6b6cd2b4da8 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -29,16 +29,24 @@ export const FilterModule: React.FC = observer((props) => { const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined; const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())), - (module) => module.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (modules || []).filter((module) => + module.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (module) => !appliedFilters?.includes(module.id), + (module) => module.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -50,10 +58,10 @@ export const FilterModule: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { title={cycle.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index b9f864b4b66..b97001b008d 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterProjects: React.FC = observer((props) => { // derived values const projects = workspaceProjectIds?.map((projectId) => getProjectById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = projects?.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const sortedOptions = useMemo(() => { + const filteredOptions = (projects || []).filter((project) => + project.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + return sortBy(filteredOptions, [ + (project) => !(appliedFilters ?? []).includes(project.id), + (project) => project.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterProjects: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((project) => ( + {sortedOptions.slice(0, itemsToRender).map((project) => ( = observer((props) => { title={project.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index 06c1aae9f40..e283112be4b 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - +import sortBy from "lodash/sortBy"; // components import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index 5dde1d27980..2c2cca53b89 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,13 +23,18 @@ export const FilterState: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (states ?? []).filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return sortBy(filteredOptions, [(s) => !(appliedFilters ?? []).includes(s.id), (s) => s.name.toLowerCase()]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -40,10 +46,10 @@ export const FilterState: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((state) => ( + {sortedOptions.slice(0, itemsToRender).map((state) => ( = observer((props) => { title={state.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 5f308fbd17a..ce0a9943e6e 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -68,7 +68,7 @@ export const CycleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 0c6ba3b66ab..268a2c60c85 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -59,7 +59,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), From 94327b83112d9e5c22366aa2538839d6ac6e04bd Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:16:32 +0530 Subject: [PATCH 044/214] chore: feature build process optimization (#3907) * process changed to build tar and use for deployment * fixes --- .github/workflows/feature-deployment.yml | 194 +++++++++++++++++++---- 1 file changed, 159 insertions(+), 35 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 7b9f5ffcc4b..12549cff514 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -4,70 +4,194 @@ 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_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: feature-preview + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn install + - name: Build Web + id: build-web + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn build --filter=web + cd $GITHUB_WORKSPACE + + TAR_NAME="web.tar.gz" + tar -czf $TAR_NAME ./feature-preview + + 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 + 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 }} - - 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) - - 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 "****************************************" + WEB_S3_URL="" + if [ ${{ env.BUILD_WEB }} == true ]; then + WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600) + fi + + SPACE_S3_URL="" + if [ ${{ env.BUILD_SPACE }} == true ]; then + SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600) + fi + + if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then + + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }} + + APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}" + DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}" + + METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \ + --set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set web.enabled=${{ env.BUILD_WEB || false }} \ + --set web.artifact_url=$WEB_S3_URL \ + --set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set space.enabled=${{ env.BUILD_SPACE || false }} \ + --set space.artifact_url=$SPACE_S3_URL \ + --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --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 From 8cc372679c526c096b144af1b9a2b990b1e0db2b Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 15:56:05 +0530 Subject: [PATCH 045/214] Web Build fixes --- .github/workflows/feature-deployment.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 12549cff514..766f3051477 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -40,7 +40,7 @@ jobs: 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_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} steps: - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,21 +54,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: feature-preview + path: plane - name: Install Dependencies run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn install - name: Build Web id: build-web run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn build --filter=web cd $GITHUB_WORKSPACE TAR_NAME="web.tar.gz" - tar -czf $TAR_NAME ./feature-preview - + 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 @@ -82,6 +82,7 @@ jobs: 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 }} From 6c6b7156bbb46fa5d2ebbd4e0d851b264d5a2cd3 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 16:00:18 +0530 Subject: [PATCH 046/214] helm variable update --- .github/workflows/feature-deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 766f3051477..c5eec3cd3ad 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -182,6 +182,7 @@ jobs: --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) From cb78ccad1f9f0d9266dda5c7fc365a59d562a5d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 8 Mar 2024 16:58:37 +0530 Subject: [PATCH 047/214] fix: 1click deployment fixes --- deploy/1-click/plane-app | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app index e6bd24b9ec7..ace0a0b7914 100644 --- a/deploy/1-click/plane-app +++ b/deploy/1-click/plane-app @@ -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 ✅" From cead56cc526af19638141f9ed25d200c8209cf4e Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:32:00 +0530 Subject: [PATCH 048/214] [WEB - 671] chore: remove unused images in the app (#3901) --- .../empty-state/Project_full_screen.svg | 31 ---------- web/public/empty-state/analytics.svg | 21 ------- web/public/empty-state/dashboard.svg | 23 -------- web/public/empty-state/estimate.svg | 19 ------ web/public/empty-state/integration.svg | 15 ----- web/public/empty-state/issue-archive.svg | 3 - web/public/empty-state/my-issues.svg | 15 ----- web/public/empty-state/page.svg | 21 ------- web/public/onboarding/sign-in.webp | Bin 21740 -> 0 bytes web/public/theme-mode/custom-mode.svg | 54 ------------------ web/public/theme-mode/custom-theme-banner.svg | 33 ----------- web/public/theme-mode/dark-high-contrast.svg | 34 ----------- web/public/theme-mode/dark-mode.svg | 41 ------------- web/public/theme-mode/light-high-contrast.svg | 42 -------------- web/public/theme-mode/light-mode.svg | 42 -------------- web/public/web-view-spinner.png | Bin 1456 -> 0 bytes 16 files changed, 394 deletions(-) delete mode 100644 web/public/empty-state/Project_full_screen.svg delete mode 100644 web/public/empty-state/analytics.svg delete mode 100644 web/public/empty-state/dashboard.svg delete mode 100644 web/public/empty-state/estimate.svg delete mode 100644 web/public/empty-state/integration.svg delete mode 100644 web/public/empty-state/issue-archive.svg delete mode 100644 web/public/empty-state/my-issues.svg delete mode 100644 web/public/empty-state/page.svg delete mode 100644 web/public/onboarding/sign-in.webp delete mode 100644 web/public/theme-mode/custom-mode.svg delete mode 100644 web/public/theme-mode/custom-theme-banner.svg delete mode 100644 web/public/theme-mode/dark-high-contrast.svg delete mode 100644 web/public/theme-mode/dark-mode.svg delete mode 100644 web/public/theme-mode/light-high-contrast.svg delete mode 100644 web/public/theme-mode/light-mode.svg delete mode 100644 web/public/web-view-spinner.png diff --git a/web/public/empty-state/Project_full_screen.svg b/web/public/empty-state/Project_full_screen.svg deleted file mode 100644 index ce373850737..00000000000 --- a/web/public/empty-state/Project_full_screen.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/analytics.svg b/web/public/empty-state/analytics.svg deleted file mode 100644 index 4f7cd4b7d34..00000000000 --- a/web/public/empty-state/analytics.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard.svg b/web/public/empty-state/dashboard.svg deleted file mode 100644 index 1e45c69ee59..00000000000 --- a/web/public/empty-state/dashboard.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/estimate.svg b/web/public/empty-state/estimate.svg deleted file mode 100644 index 604b2f143fe..00000000000 --- a/web/public/empty-state/estimate.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/integration.svg b/web/public/empty-state/integration.svg deleted file mode 100644 index d4f8ec5e8ee..00000000000 --- a/web/public/empty-state/integration.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/issue-archive.svg b/web/public/empty-state/issue-archive.svg deleted file mode 100644 index eee79ff3a1f..00000000000 --- a/web/public/empty-state/issue-archive.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/public/empty-state/my-issues.svg b/web/public/empty-state/my-issues.svg deleted file mode 100644 index 98a420d75a4..00000000000 --- a/web/public/empty-state/my-issues.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/page.svg b/web/public/empty-state/page.svg deleted file mode 100644 index 2b0e88fcf31..00000000000 --- a/web/public/empty-state/page.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/sign-in.webp b/web/public/onboarding/sign-in.webp deleted file mode 100644 index 0c7fb571c55d1f0ccbfed2af6554c8d0cbd9e958..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21740 zcmbT6Q(Gkr7lwDXU6XB1w(Xj1H`%sr+ivID*_dqGu1OQW_cwg&y4J}$eopT7+^RB? zl1J(QfTo0~@-Jm>EqDL`fcoEBf&+}f03!0r(&mT&02p4Y9R)4#=odk6#o1xxmy)o6 zneX2|q?b5=1t1Xvz{Z|bLS=*>{>p{yx`oCH#y03-R;2ZdLZ-M*zQ7Uyrv#4f2vBvr zE8UNc=jMBdJklJ7?8gq|v_Bkf3I&{6OLgBs(2TWRZzw*4oEhaIQ$=6xbS48)fBqZsBZ}ZahPryxzPW zw+jg(Nv$qc?0&nWpG7bSAjz#RYwr4b+kc-{?4NrBcTARWRGKf|D1mE-i-to>Pwenc z#P{Zf_z7>hitpt9svQ19N$>YY92fm)csmhB9QI(`W|c(bg1)%*gT@Eks}L(4ZhzXm zgT@ZQ=pRGODm~`ld!{?n`qG#>x?uJ+3YYI^0yTbU2-xnU)kCA2Cot$0J1^J0QN!-7q zw)~>XtJ6MnnujA-sR#t4S#d%%DVL+84-^6B1F(r{0U0oihsY9t^qWWK=g9gGFNTYc z!RJ~N`kxjQhei+PbX=hfnu_ZtH*p@5x=Y#PcTfm=yHtPq2ExO0QgUgxq+5VunS;R6LmGF*X@7v5TZWMd!oq<=% zS?TDYzYaAGl?9qrhfTs`9zSoG$KQQ-jb6d)0@%SAew~Jwc09MZ`QO5Z)zl)lt=j92 zt7n{Bv9=CDR4 z)*LQ3hTwu|yorTetw~~*c;gG*Z)qq2KxAH2O$}fYqiBD3v1m-}~ifHY`?7 zP+C$rPBS+^0LxKK6Q$U=UR&g2$C{r0;5Ek;TI%ziY`UDW&2hV@+W>;ZesE=$(wzjY zsf}|260jNL&%D$s`JRbSAnQQ!Cin-3w=N_j&hk}TZGZ9>UsbU4B z+7X=5jI9@Do<hTj6hDSCicPK2wdzuj<5c>!a_^lIWU#=|%S{Z0TtsB9btG7mF(&QzQ1G4KP=)dWum8WN*e;4xle&Y^bSzAB7 zlKth?YK80tl^((U7hOGsI)bL0TH9j*j^|ZdjoD%pS2$7y?mDN|>lS2#>uMH)HU?91 zta`oYeyEZw2~c#V^`#eSRAJ=y53ELcBMw(H@l>W9JUN0=tisQp)=*}X>BgZjx8C%8 z`#Du`vTf|PRI}fKaAvlsDO0R^1sBb&Z`1X3`jUx|P9V@tq6!YxTcx>5(@*r%ePe`Z zOSgOU)P^7EQp@L)q`dk-Mk@%17;M>;`07L~euryFoT%E!8YTII>W1~bMkg``fzJkX z(KXu887yE4(}Z$wWa64BdRffgWN|CWq*yutG9))cH3A2y+i~Rq{)eW#j02%2Tpg{U zNSKd$u~j*KFdc4?pSi(4sD$1$)i~zb0frIe>Sk2&hJaa1;#3!-6QRbS(l4@3WiB!l6p zJ^X7Dt+}WUUcqjXzScK`vI?%j>EJcPO961AcN!uOv>dZ>T{yy3&pc0SMd67pc;H2`fSfG(qT0vO*PXnb{0ta4#b*M zQd0x@47H479?&R?&{pu+^ve}~m$zW!+&TBF!&*9Xw^vSZ7d)_;etO>|x*+e2ra+N6v23SDCo zku!IyVKAd*)jA!-Hmg?YDu!Y|vxd4bUTzu*vu>YUg;X#EnWaYFJySUQW_)nh0(4prCfm+5QSWN+{L3A#4#I^Il1m-Pi7cjx{K{4Lvp zPtTu+QoH`0S#6gQ$vFaKXmg97?YI6tpbZ%w|77`)E6L&9!S|P&ozBnI7)x`Z%%92e zFtO!#4PoVT@}hz0WXn*Tw~H4NJ83-7PHdl~4+yG$p(uDJtZ{aG{Z; zL{mCMs+8Qs5LU4{UnD;9HLd>N^C$aI`1wfT<2s zD?%U}m1|r9tUM0tR-iIXq$)cpWuzzz1vi+EtrpcOLm3tTYEr38Tf~K8s8%mSIT4oX z4sk@qyQcqG{Uy*0)dK!If#+y$E+vm2ADvxKpTHHU)5Bo}q_X2#o@|B)?1V zk|=>oO>2uS?KbMKF)9}+V}eG7uVVQ5V3uWb%U1o)jW1;wP$c|wturo1<-t#5@93!u z4KX_cP(`WCp65keZQlbAN39G@rSW-|pTs}~?*)ftJEqwr3#D7n2OOxivl$NBWP;Tt zcB9HQ2X&`T|MB^^q)%gg`ZTCVUSvOR%v72L`H)!q)}i^!&h%3P6bh)*>!d;hpQ;?r zCPKtC;FshS?H`DoCJ+3negc`HI31ib9ongwp|(vOHw^X6yXHH5^>l|6^YadGHk*%I z*@oPY`WL*<#-wzKb?!%27f@8bbg*$VZ{Rx`j6D(iOh8r7nH^K!>UK!XbAUHs<>k9voYX0`ZT}w zZIsnTyJ@FgFc9HpvF%ByzZ{wpigQx);ZFJQ!Dc_{pkBB!YVdI&xEr$kuz~G7@?c#fW8w`5w|4? zzao<>*<~DT4e-NR`d(>IQq*KZ!c6Q9l$3<{(8}+nGX{#iK$g9aNBdLZG@{vme2BffGFS|< zdp$mR1F}i8+DAngi?0p-z?OIkl#XR)e=sjm%K&S?z#fG+%a4ZJzH@;hL4wbS*IAWs z!LQ}7+mF8Egf*B4q4R<(p{_3_-<+@ZuZSq6g@EDjg>Sw)s1L(8%{QStp*^AJfbj4C z=6?2__=EUFXxsnfUHKE?WA(G)p)WnaC}1+6F<|i z`fyoSf%K+X8_2A0Ni&LaUP(0 zCCP4tW^^6*WMs}Wcry`V!v5<2PU0yNKutT;iWxuoJ7swnSzm({e( zV?*tJWUJ`#Yhd=09(4Eb|BS-~_(rMy#0$hDLTSZ;c75c1&2Ahqs=A!`iE@HYZMs!1 z9f9r?wq)0&t4}d+SwbDOa54vhVMsd!>E-PBfIG!)Q8sY)Ajsuq8 zW`DtO*S8Yt&F^8Cg0Wv6h)r&3;+l!K79pxwo_^E){n30iB^SJQvDrQzZ>L&VB48>! zEou0dr3XF+tnepOj>a zN<11}jatRF`93S%pVbvI3Fg}mpN0EvSM}^{7836!5EPt+QyqOx>6%+*rL|9&lJotP z;FJP{MIq0aRnyAxcM6K2!>94sY?gry#;Cy$S6Lk3cRVnPE$ggtjI-holE{8kr5{ed z_DoiGpV$nhmPsjr&7YTQx(%fEz#9AU!;9KviK_f&90yVVg5`BUo-A$80G~_IhhmOz zSr4~yunN1w3QBYiECu-MuPFQ(m44}M?OT8Gm~e|IYFkI?_c#IY1V8NkiV|VmtwpXR zS!lZu;~af-E_T+pTzwdoT{(yaf0UuYydr3+HWhZs_F+Mcr1&~J@90E*1afI>JhEnK zJwiCDz)7=m;oRtvU1Yv{(#<1UIV=1)p(A>_8Pt7$lCl>85|R04k#r}A<~MoQ4aXZd z-eMqHK4IM!_2OL6cS!grA&}*i&1i4o>pE_IN&Z3gEqc0e`A}An(DYl*C4=$LA4xRn znQpmtRq9%Co0)kKUHS=2vY@0%h13A6;l4bPd0N8hyK0&oFPt z-Uo4vNRoec=0IBjUanflir+?HG0@I-K$otuV03F|L!n3LGU&-DJ4wzPs+K_%^NsX~YjbZ4Ku%0|(u}nzmxg^Yipqsj` zlA+ylnIDOrvwz?d$xooB3XDzNV`OuByG*-^8(2xT0qjodRHhBdmdgc}D7_F5!UstCq;lX=a2zfxPB zAakK*stC7AxINn$o3tF*nx19ITIhfi0fgFPUnb5$w6Ceoc<@X3Sb&V+c?v;6Kjqs$ zM2o3_`#7^SD9>-br)f5}wt2AcZJ*p9v5kGO9S-#U4_~-@YquXS%Ys2E|MLpReWhh~$wLk<01QNOh!813`|J&J!>4nYZ{i813rw#%{JDYaT-+Ss~V! zw~(lHN4r(PjmaG^*xI_cX>7$Os6_h`J#BdYS@TKI-96&4+fRHe={GvbxF$d zJT>+3FV0|tii&a}V;J67t_tv#ON!%o&xZ`5V#&+C5~9kl?m97j_OG(6*nXtEDl|B% zzB-?fJ*&eTTD{b;3{aQAPs{1l!1LOH@F^LoVm@WNOyJm*MF9q!j&m3F$J@Z0{R4HP z&;a=ZFWyBbnaxYdY#Z9%pP7=9h=*EPpX|a-{M&0Dnj-cbTyRdT<*IVyEHf;%V?=1a zK>_y0qZmkcqgH_oqk1jUByH1YybOZ`!t^zKToIxn3GE%Nuqb!^5*_J+MQH|3Vkz7t zNzUV{XBd`pq~2VP)|vE!p%)&9{72LfK32H%???n4T1Ovf(EDQ>2a6tNg^dDTh?B;= zaZ=XBp_GPuCih12EWC{UMQYYzsJeb_*inv*%GwyDOirII#-068M|gpNtCB}cPLwGa zH}7ueguH-8`wbDMeDUvV@57rumbLz>G{c z(<}n+P=d&kC0TCi-lF8z2P985tJbtgk)~9-ujtH}K7dBt`L)^+vBE}NrzDvxTPMmV zEoFfOwL~XRTh0HrC+XNXF=d1hl@f`~Z@RMhX9XCd;KZ;A? zhdr8#LD>yZrG+0p|HkdKdPl*y&a&F7;VJrUEu!esba$%eh%aq=X*6H^(AQj$_y zA`$B$CPZHR3-GX;gwU;h!8w;(b<(LBp($u;1t7g(B=yUaaf?=uapWh#d90+(K1YlP z)I_)_yvPi-S2-Si2evX?)bHM4fRfzm3UX>e-F{I$f9^oo^U%$*uOH>;0OaRvZ3(-M zigo)eAHvr=kcTmf$Q(lYb7f(z(sk%i5U5Ajfp0z-EF#(K-SP3XYmTXR=0f}KcP943 zQ;T0Vvf?+~YKO@yCB^%7%H?R>3B&D0TBKL@#F`pcFP#w~Bw7)xxt+K7U(7pBzbYmo z`Y-D=F?B*Q^Q55Z_>P;Br*cyK$>k+3yr$~4&w?v|XwJ;2d$gx~9rZv0d?L{v0UMQ? zmP;zFwACLIn_;@H5Pkl{vRmh}G)8p%!;@>xuP zg~rDX5NY^#)!3Hm4;1j0ton3@0B3fPQTw+p9(G~}Ce>Z|DZ*(ofM~5SOIz_tgALfM zoUKfYKTAb}*Mvm0)y+t4U!iGv{T-n3I~VVj3$$`2Etoc$G5on}3^78|m-D?}{Oo~? zs8Mn>PPJ}gUkbq$0N7_zOkJ5&u)P27C>QDB;}ik>dvKboLETb^YWz!)42pHcS{<+q01?aBZo5}p=#=0lIm5!L%}plE!-;9kt8+)1+-CDCT_ zjDQ=AE4Om+y?c+2^I?xJ*vMjQfKfPTYiB@BnK9Z30Dy~v!U?)DMf`=9@BILJS89xS$5{XcHrgC0X=S8$ly~Oo5_ZTAc;==y-Aj1tIG*T_P4!^D)V7;+-p-O z0jvlQzVEwdn#Z^^))-@_^3Tz&P-~H?+MSYt*0TRj_R_PleA<6DJ6{X4$@_yp{+!5o zV;X;YIv14+(5_`7nlW7sT$)=HU7&s6K1w_4ExhX||{mF}5L#$RU?^F`2(y7W;9u z%Z!#$9Kim;p{~qdF*btTzA-Yr^}eH&@A{5)|Lf87EAfj&Q-)oL8qG}aBu{;$RPtBH z!Ix6bv`5J!4DPqusk$?Gi*)}e3&U-Fc_75zPDz2PL7?u}ST zmX-m=9TC$s46e6ck!qpcNiELa7TMAG`ye@AqbDXyX|YwQmQCz3xurFp;z{j&3+=C? z0UGn))7FSx_!LT$Qd&#;m2ncfG4K)`-q4RYF0U0j(oTmcxxZT=a1b#8j*U9%BMtdD&_=kwS~Ie@Ek3w+H?Yid&q>u5Ebc0Hihl& zNNV|x-)qGu3w7&b5mPHUw_b9@zq9hu07jj}Lf?9IZLbf}4h{d-&)Gn68?cQ5e26KP z%}Afxg#hHi%pbq#LbUp?GaeQUlxJq07H=yQF1ZrLHZ;Su_#GhnjXo|O4z2+y4phV~*|8SjP$Kj6`1x$Y;m-%u3otRmi z%}F^NYvO4XNPOd#wJkmhBr?{T;lF|aWSYzZF6rE+|MVQy;J@k^hx9)A>1+D=Xh#MO zfA#CtovS%ts@Uomnipq4$YWj3# zRh`@JTO|G51f!xu)NDFb)sqCQcv(V#A(G!^v(8IY!aFl ztK*y)Sf+cI0`bgaohcXWKbyqyL7d&F$DpvF%MQC8R-Z&h@)STp*axV3OW{t$h^j-> zew!Gv%;E`-tDi&|G8$KP-;oG9%Q>9j`W?Xa#d!81YzGmKghqAaYQ<*2)Mcibj-j5K z;W(Qfh|>#Thd=pDD_28&gKRq#BJ1Du zUxRJ6bu(mVMzmx8YGD>2M#@AO{=d3S1g@_Ik5Vt&@u9BNeNp%SwDb<3LLVTb^yMut zdN0=uB)CZJpxpMm>k8II66GNLsk%+A?BPIQ+pjt_TewHw+yw}#+cW0(qc2zT(`&5} zoWszb&0-LWl9HiC*Tj~y!)lQ}s@>1hgKZfu%&)gbT?3o)*O!%s zHy)E?(=J-tR}Yx;^^M%oWTYXHLHcPp?>o`mPfu zvMcpMFJp&=pS60bWVwPxW!R(2$iJjCiynZN_{4J6(981bCW)<$b@tyR4yUKFE{0m- z6hYZsKfFz2bXcHpS>^V+k*)4Aev)7|e7i6lD?^`v02nnQcv%p-Tz}(j!=x9@hW5Gw zuby`4PFt13_}>how@nu)+(P`s$&AP2-w>xw@a1&T_MlpODO8;Z3dV60DtoLKPFir} zEfL+df!6ccs)d4w_LZAb;nwYDe7xL+Oxz1z8VGW6)Hz^>0VeCTxgV?D67%G_f`SNR zv;4T&*7^*g*;ZUZ@R-CSfpM=ig+fZV87Kn0+At!25vhiDw)n%Y!wdKTGHC42m26ys z(NrIyGl|uZ>7bg9BTm5gGevP!A4*@L93*V7mfSQl7QL%_Y+S!T^%5bDeNGX2#|JX3 z%-YLGaN*w~JZs=2NAC{7c76w2?JKho?;d1{CNCnmst=#tX0CLBBZ`Bq?fsjTHT;!q z2J3*)!2me5{9MHysaBd)@Oj=pe;$>A>iHduSt-?eIc@6wuT(y3>n~}e{{OH(=C`>rW2fNN z6rOEYHPG@yu@eK!B%m}0hrB&HAnKW#>mZ&N7q)t3PHM%uA13%rD;p7AHMpv&mKzJV;c z|4VYc<+h`jd^V`EoABsC z(2$(OS%>_xTKX)D)Ch(x2=R+M#1LW8&aaF9z=6!HmsS&gJY`fI{8y&eQD1ScV+$S5 zDmfwPpj_L054n^~BgG@x2If5@plu5MX@8nsl~&rr;h$1UO8jf|d1 zJx1^RWkKQ~c{WQ3v{qErc2%nk6Q&LN@TXeOR~i6?cL3Ac(3f*@{c8t}1g$qnz8 z8P5hX=iY#qBn!PB7H{!V(htQzxp!G==kX$%j;{tBx`$I zN{B`LN2mD}!fCN9rZAjjP4`|AqrSdhy{9RWr2l6(tb#vI-nqz9fSesS2p-W>G-?^e z+lZx>{@)$O=s~sE7z>;hQLR5Xv3t8W1CQQFriE*tq5fwi3y73)c?Z?lrY#JAF)i|* z1}0D}EV4)`83IVFdw99V7D;%#z5bN}A(qgy%A9WX>Vj!a9lYQQbG1CxR*Hzg7a&e9!k60FQ?^)`eJDBf$&{}|s&9~?yJngmrk5Y=W5 zP3{NXMMia~YfFqoVPYE04ES={<9DNE?S*MaXN#Fht~zjRhD=Q^U6qd8a#hAle*=4=%KGNL4sqm zgWS(k`@XC?cPBssr(mwMMA!)*MKc$h0{EplNA2w|1QbhnsUkAM{VdAL>s{!V{jD#K zlEtDnhn`4YU(8>`m;?FI&w=tFvlZ6G z?w07C{-!#+p8t7nzPge;xg5 zjsVtyGt6l;X=B87KuePI+5L)Oc+)I*T7p-Xa|D=nNOPNGk3Ua}=lp9X@dydKm-yK? zIo(bBNh@B4BErzCUo8BW!Yx614kvCGOrn-SLl1H}7r3!_J+8_55*>f7Nb>0x^mD)W z@1(SI)d}2b3(fOE7VI6kg?{&fj5f2R)?>+a9E2(o?8^pvL|~CGqo1`#G-Kuol=U|4 zi2vJstAgr0<1@n6S$yfA0JlGn9C$hhaUiMTvnHV->=SM?Uw!z>sk{`lpIRGo4dq2) z4-F*QaS-0-Mtj5MNbXy3Yo1NE^QtU-DHddfV2wO0Wg|)wH#B15-NjtM5UGaBSOf*- zH_ru?sWcJ)(Dea=HZ&aR>f0EWTdh>3^O2*2*aFN05Sm@;FDzp!Gl3HV^Ka9;Ds=3&cFDs)|j5EzmKh( zPgSLZ91WM&lHBAMqPZa6_q9K&^NqvlEs`PbKd@g#E4ex3w5NRcfz%LUWr5wzQ(XFU2{>^;L3Oyez)q%#gKc70O*bMdg()v7kx9!%y>Zibh3qr#&P_4sb0%`AyY zlK|RK^?s4k@2_O5rL5L{d+C|jH~u;*I>Bst?~T6T#5gO9A(H++n$`0tYL0xt$5bza zOg(tJlQ4TY^(2D5=T2=Qs=)k$Mw17G$Qr6%q97Kh(aB$P6vmUDFKEA;J&gRjljFbT zZ&;dz=j78&M)E$4sOg8T4tfW&B7)|itHCSqKq|FxavRo3l(4lR`xsPI9DXH>b9q#nhm2Do=c_030f(&n_VV0m<*pXoYZwQ~hS;g}Vuj2DE+VC3LFzx_5r~To4>yVC4s89=6MLz>5~A*2Ih^ ztn_^`9%MXXeYsEXMZ?n#qTMmVmz1$tn9kAVu0Kf+;+fo4V$pK)U4ke2(a9Bpv_v!> z$E4xwl^rQ^5|5707?UOWz2M5Ko_sA-`fwYFB)PS_4r(Zp z{JsXu#VcUjvcoWus-OurZK6hFX@fpawIIIbodm{9{;?wU-f0~le>2$fWt3I!E`jF^ zr#Cy?2=yLhtL8d}M5n9&F!*b9z}(w@CX@@i|wmooz8FwkKTenKQ$QE=c}S0^Jp-z;>Z|P21C0Q90y4!59L`G zt@c2@{&0~*@@Uxg*0oBrFnew~byV23uIIO=C>m@X{rF7@dnXXs>F}}PbTn{=hKOP! z8Sb{w8mdrkwPfxo|LQe<>WN^s2@u~Wzv;nb^k1dbQ||Um8A4kVn*_h z|H_y6w;AXRp$0=}5m1)l>P4cMmu$V70`;#+>fxfc(cE!pu)OfFyoYuw>XhMns|M@t zb%V;+hx0BCXYfdQ5-Mkj4`D_pwnJccco$fg$kYpqqZ#7U23VEwVe`^u(RCHj*{NRI zayDcyH+;ms@ovPZ>rZ$tJEwZ8d5Zidps*%zco;AduOah9x7*F>4d&@}h9}kXV<-I0 z$ifYKm6Dxl0)kEgKEjPE6;?9CZSilwei5P+q041#%Y+MG)UspZEd}~}?I{>L08Ck8 zdSA<=e)Iewyn$)Kar-ow+2+CBKvmy& zvuKil`pLweIs*R`8La5=>0w^6V{pR;worv7aZ_MxL@Cmo-2D~w-Eq<2qyw_==V`Wv zp(Z%BpByECGrYsXhDr0IR>7pae5?1|c<2YfSs=s@_{?CT*}Xb=(vD^O*Pj$Zvoc3K zbOP`NQ+jWN#%Ec649TC#sAfqj6u^u}1r-T@#OnnsP76de=-FhoIl$C3L5MoF>!CBe}gQ;vzmh)Ui;jf#r7EkXEkluZnh0!QXF zcb^`uy}E zG8%WzO5a(opf3Dy?+UEC?}?uP_iM6FtF?&!NMH?Be>Y5PB3(Uhy~54gqFdb zm^JChmT2?ib#QlmK>;E)F8|6rAKYXwZE@Cy{Z`X0KS@4e2j{RObHGP>jR6uRxM(lg z5U!>pH&^>pR^<#;>{(l`gZY_rz?>TqtnRq-@}InHruZM(eb3)KjGg>-mu4BkqC>k+ ztQP_3jwOz*`lj+@sEQ@UO7IUrGb8k$C~_o3Mn{lhWQRBwK43lEuav(7sti9-shmS7 z1n6&5PyM8F$qPoo0trn8E%8g%oSNXs&2lH=@SeFR2KMjFF3C=;9<^Yx-h#fl;o9d$ z1DHRHHr<7c;)EdLE#X$JVGCR6vG?<;ApUkHg{D&t0Cnr0@VSlm6=ZpM1Tc+`9^Knt zeMo`;gkWwiZ@-RYC=1%oih6z(iNgP$M&ZTz@=V7{9vO!oi)}vPVfT;osa^*LGSax* zmBe?K_!DkbDPulj9T%q(46iq(gpf3QPFr#HRN%?OA^MpY$^liZn_VARuG;NeVvVg~ z5(r8()j);t0pe&ogh9*S(KY_Th`jVE2P|nrpbj1if-PoOU@z8)CT3Krn7if@jBeJM zbrKRs@Lodv9i+fq^WpeHifeLPxX^_2o``~-RICHw&0Q8hNvC~f}ch1MV@?gQpnMQwhBjeRYgMk zPGHmQ{!QV;*9`L;fAx*N!BXO}DE4JFXS;5U&K>`i)gvOm1zC54=?~8ji}+DMn`2|e zHIO6U2vpo^U;RSH5&d_}|`feMw%;Jh@85IAxWP#ap5=b+uY zWN_j#I#2gKAI*M4-CzL)dAc(3Pn+?6-Ym9#uQ{ioE3%3PVg*SR3oo zl>yc>!^vnP!N?{@(Z=H?h#kwM2s8 z)HwaRq%4VQTsL)Os7Rs5nn7O2Iv92GL&%b=f}7lf0S|+`iTkROHz7wf8-W`6H1=Hd zDM+}N3K%)21*j#c)y!5TpPZ%#z+Dh=RYGdP93@O=A($$rDI%{{ob6ya{Y}s=_1@8J zk2^%jbXAVQcF%Ly?TfN)^fW%K5MD>ln7yGwOJ~ot>WxMyODutTGN_`@eb1%&vsX%Y z@yHJo4O(#pzFKLWlS3O^ELMU>LjihU*OuJG+Rn9(c*Tdrg3*+Eur1S zW3%k*L%d7E-d0}A6m1i^XS1UirHgb)yG2{eNWe3W{^$<5A*i?)oCbS+$eQ1&N@Pia z$&IT>O=iTLgV8BjF<}#(k$nx^V8-7Ra$m9!Zc>l<<9fo494|I{%Rjs!f_`2Fw@AKb zjEfspUMKo#uH1SJbalpq>BUO6dKSP6zd!thC?p7xq@#&eN)m+T2AXNJHc~WS%pm@V zVaG(RfN#|AzhTnf@?$X#x~VVidv}kzX-y}`JO_C)RqpNIST<(zVh1L?uRNyH1wz1? z2Z1x#%gS^fFNzr8Y&)1VK7_(2nEgb-uS#OqGTym|Mn`XnN4ASVSJ)}vlITq_CXu*U zNZZ)93ue}5cD6+tIEQ1TJzZRrzV>FQY=dJh*t(}(xbMravEsW$||t?!<-wKExFV{6>y#pP0n$!WZhwOQ3w)wKR1ULE@_Gk z3J0|0uUwh-J?WGeP8q5;2+OD#ota}p{Hlzit&YZ8KmI`HQ2Kk47lPdf-*dOSDgsG| zsuQkUeZ*Z&FHec-qehWWNR&&Q5X73W?bsnN343OLMSjIPMjgQ^_w<+1;;@~f$xbyd z5dcuAy@jo~F0<4IKq_xTum*JzABiXS(ySpdbqMmV+^Z_+o{p#OgzvyYrw}9>m1^th zuKbc9gU&$oy$byuv(^&`?s7_UZi+5*08!G%XR>SZ(nHy53GC&{5x+~Rv=@c^k#uK0 zP!Q|b)jV2Q%j!CzXr{Zmj&3WIVgr$<4YdSu8+rLfO>OOckW-I!AXWfUrI7@mmJsca zfC||mZBKm?>dca1-7FE}?1L1%q{AEH^w&WxVK6E>Gisuie;Kg7%xlS}mUp47*sGIy zrK$p_q$wnT?UdQ~~>h<Nwh5N#v}k!hSyS$+^-661z z@cCR;QVt1zTy|_od%ehSfTaNzh}BOtbq$CL)kmd+jmeyY<5V@wR0b$6o~g0sxS^JaIR(lciy}OJ{w@ z97b;Q)Ypwxo9A&I%=db%>Z8Fz;Bax3cgGBA$Ry;s?U^7t_gV)LJ{HK0BKQ$|I%HZ( ziDkw;xR13c|Ik7xEZFx^;J4`G_=&ZN=4Pj)T;S2sIxYU|g*M#!?+-v;uXD}hbI8IS z*DLfrT^^#&N)>f!r5K5FYa6_R6~WO4P2QrE*Ss)H6H<~lR&z1$Fpr9)*WqQlaPF@^ z3uZbS=z?whp;~gO69MnrplDw5s5<6hhcdZ17?K6bSxV;-%fNyrW}_L8V{zgH&G4pw zvJE6Lq~8S^d3<1mvUQ9Z7BTb*T(k*R{|liMUh73iF$B4JkM)$6+HU^Eg;$w+J&}AF zwftyh8`E}kqd2smZ1)kInfU1OB8P6IaV_T+Z$uRbShuAD;bc#i$Z>>Sy6 z`?h{bsXzihY35c1=N39?s3(jnyPC+`{EuSXyM5T$Cgw4N5sTB`kpPC1agjDmtMx`L zbaZ0JE=IZG&$`43>b(n*epU{4x(;sQzEua#edey1Ggbpxon^pSo;*=QvV_5x2LR+a z@a~6-7KdPSwCo(=MTD_S`Lm6ilWc>ENc530SB-psbNEsJ{KxD1TTBw*%bTu-&cW6qW? z;`n1?DS{x^Ff=zoVEzq*O~YrDx8#PhKSW2nIX=VEtK*i>g?@rC65S-gO*U2fJb`*l z7*si$Pd1uVm81>A4up_9Jk8t{1f#?CzZdQ!t2Y5P$FbI-hT<`Fq4r_f050Nq3QG?D z`DMy;a+tF*YRDym#Y*rof)xC>`l-8KqK0HDCOTC#RgBN!WZsh^uBR3@qFf`*C_krW z*+1nuuQ=*urJ9;+`zt`0ifs^mAX5Ai@d&)%`ip9$HvQBQh;+aUg((H*Oaf21{jpslT1Rh-{Z25X=x3y2QM5fQ2F)PqZd)vXA?Bx{>$#jd{1;0CG2c%Fak|4*VUC7Y z>x&ma^&%Wys>dgSLbgN!z?$q6ki-`3K^8$?pO&rA&mL8>tigt5VPchr&lcL94ekF2Qj#A39jTR+7=P zSP&mgbX-YT*GBBv7S|IcaDJfWu7TKPDrc+ndbS^W$ekvHf&TwsmeFFU0AZiX_0RRV zu}<5eX82zS{IL8$EfXOwL8O7g(U4JA@CqHrgN`%RQW3fkyvtj2%3g@*SNO9@i&tjA zpKHCfszoH5V7R~L)-QcPJv${tVAF}Y!JR`-1G>fgjkzJ1;OOSdKGD7h(b`q49w7uY z5?s{$D74AAWq|sbIBCz+lLIHu4_|Dbmm8G-ra7QxaQA_X^EOrkm~Xksg}gg9vL@0f z2t5aoFj=%bh1W|f&zr2o7kmlGg8N4Mru?`yeSG-u(NPk42V==pk#|B;zga%%bhgHL z#`BHMy*X){HZUthNTti+ZbbfCBVlyWy_U2x=%rS*mdim3UK%S?6~#A#%)k-{vv!Pj z+;lBu4XXxX5FtE%2k|igP10%=^npL6$UiGHBl%32O}Tf~OH5sT$r!s3zqqsN7D2pS zdY_ZWSZCA3s$4W@T84srwayxy;Tt(lnn|GpoC;3_tfkL80cr==c@9SI{I``-W)4ys z)Ngli5LEsDRt%A0GoaNCI7NDl8w^|@%Fh>|r0pqUKZe z5A|}zJlBzSmzSA_)1u{m{;gbhncpl@HmGFx(C?mTUo__4nPt801cL?#*8FoTzhKeE z-==#hXy^t)fC(W8xlXhdV6ebO?Q;PNulX~xcp)1bg3*x3g&26BlmjHB&{pD&?IFr( z(guBRiKE*fa(6^H2??r7oe_Y69SLrNUrjR*8Zl9#{1qvrCbMogxSyt>9uD2buj%%9 zE{IE-B;UHrG9+Y6OG5~6h+q$GQ7NyZToO$}Hv$la!wqH82hOf{>gP$g;h{hl=hdEQ zDU#Q^$-lvbaJg6j0BjO2*;v{|z{SIA7X#r1b%n94;w-?dNh^X_@Bj4U%Mhkvj%Wsf z2IUZ3AW^c6&e-2JQqBIy$K4i9y`(!H=AwzJI$K?dxo{fwEpy3d7tw?Yibk9^PTauB z`=h+X1y>$ZwfQuwp$YU=_>%_=BHcCRPMbKd)=QHeHR%hQ8X?*fEJ-V7P%0Lt6kcb) zD|>{wOYY(4DIyC4U~i>6h!i1z1a8oq+L8b0%~$r1 zY?Vdd5kqVTT`n1G>Krc1*Z1ASrSXx(k>)kpo|z5HHv46(E}q(=6e}Fg3$!mKBTJ-5 z(`!C@g16O~sV0;_;PPjKyFY0p{A|25 zMm>eyw1z|rU;ObP>#CNR?J$l{=OkiJ^ck?+=|ef3k*GPxtH`dsXu&0P|1MJmOnu#Z ziitomhDgE@Z(=jhJ(3=*PW+nR@MiKsu}<V^igxPtaI5~IJo{2zxqmLO1;vW!hXq@0*{2V}8)53%=W6@B%A;`(uGslY zdh^%{4wd_SGqPZdXkSG^CX$|@e;{9Jp2SV(%!tv2Sm1ZnQ?(bdJ8JEZb0HkTywg7% zzDoype35A+1Z1DPx2=Kyzuu(~X`;pAG&#Z*ufuK&7cRSR7H4dKzxZfNXYKWB5a)xM z5$wjC>b~BZoGTwSJ8p<|!JH^teV!Z4Cd-deG0@^weZA@LSZOOZ67?B)x839T_o&g; ziom{nWB*d4!>W1={}7ZA<r23Qp5JW1@PIh78(`O#JvaF6Y(hUe`k zz`kA&UE>&&%pR&840Dy0)HSc&Rhw6S?}M&EHBQuK4wr_2cCp zEf1J1h;*r|mD7kvb!jI68U+KDcW~!J3o?toZC!(D4#|N<-PO z)LzPvKG;1QiRVwJvD+BD=fw}tjY#jIvYjmn(zN%OA+}d9k9jWJF)Ar?Jq7z8Yfu?w z;9BkSmO_BLi4_8N_Z294ZFH=g#CD?@ta%DQK4Uq0hI6GoHxm9p)pQ&}S1ICrL<(^n z$9zFDDmLr~L{`-ct*JzWtlJf{JIJ|$f>Z+#!EyVL>y%}BO?=SXda9Z5&*0u!^VWiv zICckG>#tVzl7+MG!P)_?9}nQ-Fm(7<(w-0HRnR*jz#T5ebODcEM*Gh=e`0_Z z>-L&dX$s=-v%?4Kr$7vxxn^Ps;6`>oA=uqRI~-%A*|hP>4Ao(6S7W`e15}6*3NULD z4i*UU2q*y1rA0Sln8aA z>!jzW1Fevyn;G%G#)5n)IA2>NT!^HN?5Ug#V-nkv00?xG5e&yJJ9{r4mL{N@Gw>AX z>K>H!%(LCW{O;(7jQm1V*6C;g8pr@6^h>HjTUsV@PsfqGMj7m-+06Ediv_-BA4m=7 zsM8)<8Gs+Uoxze0Xa~{E8oZ;_FA-R2D0^iftbx>U8rgct9-|B}l|S}k7|5rCGEZOu zXc-r6M{gpcXA1!W8vvRRAd(o%E-Oh61$7tR(xX++vym7z*3E=#64h@a`R zcva5L(=^{9dM=g-2NtA8&XA8GS|R|d^^=C>d7p; z2Opv^@D4(=tk_bEITr#}F6jooO)4XT)Y~I@Hqg8=w5PyIO>h*!p)bg&=`D%M z%D9o%JO-sTr>MHB<-$}SwFL=h2;p=#EU zp-zP3P_Hj*+pp?E7ALKK_LM6#*b}h%UE%mohvB_y^+=(9g`jiL-w#XHT@DMfItB5C z@p5v3BU`6Z^=!4u67+lOO#nf@pq~B|+5yeW02{u6KPiAB(%xh($dNwXr2+2e-A?%> zYrkT3wNdVKJ=9Z@Kstk{19HVM>wp49hDC;qKX(L!6zsgho*Sj%pwDw1R3k5_|9Aic zDp1UF5CL`$>62L`v1T%L1xy>8lvhC1MP0~@n6Jcx@mwAN^^m>{zL%)gVWgLHncj9_ z(YQU63>NGj?{o5(TYRhy02>{>XQA3Su`7HB(nFvP61Pbs$`c$a+6}Y=H4kOfDBLC| zI0GPvDEOC=(`y^4{gT*;nz2tR2UD3496AxY!M=O@rtVk)Od5hfyZ7SPB0RUpwxGI#_NNlDNRB***d?`xzBR2Zq^5s59axkzqhEOCuSa={*H_qH{7`a zOf<@;#OLUqQ2qqFN@a&1(dWZLIldI`UI9>KIReW5jUItByC^zA^hOIRQO zg-$bn(C~GKrMU(@(uPLI1!;1=2$C$YaU17}sL_))NluZwO@_e3;18ftC?JdlEzvfk zdzg=o?b8grcL4fK8Z4(rSIMGT8l=mND2D4K!PNXWwX5ZO?T}7=dy702Yw| z{iqO#`<2ASN=wJ>zke?0-A?#1wcGjYI++#KTc<$?C4)>8W*pJB01HE@u-&7wl=Hth zDKS2&MmaiV=_mLJLfk4r{{UGZJ^--0q!~f{6~cThc|@;X-@SXEOuLgkbdzCuch8az z=TG)nMgS5lh>EqBp-v&Lkeo^g4S++q2Fdm{Ub^?ne94UIj--DiNVe{&K(`oIRu29@ z=I9xYt*BZ%+dASWG9hEKb9vfD3W zn|B#^MmQA)1^XPaHT6?-65}VdCRM}Nx*!O?$gm~^l(8)Ht+7fm;VWW*&Od@>j0Adt zxBvk;HX%m!z5tynKdwZ6@3;#kB#ABE?~%8fyLwveb8=T{xrSZr3d#y*4nb8&Ae4r5)&O&sNJ?zar$SF4vLd48hYX zsRN*3nHJcq*y+JI2!z4h8Z(qEV12`=81ktpVyqY$a@@5J@Oqk8&;d6Rv{|+EX9vaC z%`U9kwYcAS_Yft{YK|+q7k!tj!0gPm5)9-oknqR9j(GRTNJ%H}2C@##swA1Csd_Nv z!Q9g!qTMr|niM6`gS;Cwmf_zNytw+g9_@2|ZT!H;zJP$Up}GY9X=bGqdYpwYSWZ20 z;cY-@x2t~~@$Z!WLoY*-MjMY*4uLk;0~{!rcA6v6&N*3@Ty5hHW(PDK+DfK!hwk#K z2T-=KLChEY!3h_VR6nJS?+O*cK%Q+Q`qAc%wX?heJlFhqVtXzgyuXkci_E!Ev8jx; znbR(GYv_o{BSUaJN|}lr(@o%XfdL6~dUJxx81g>u4yYUTz`zPNb>+3NRiq<5xwiTl zPtF26&7*5S?NpVy6M3!iLQC zZ14(cMHz?+Bv0Vkn)tCY@We(1Ut9YAl@0-b;eM06JPp}0fJ2(O4Fjc1;o5)8gb>!a zo1`g;;FbUCD5qV}7yFr0br0IRap|uc=_V=o{l#+9yu#0xZ3k6&&h0R7oX~C!kx>JF zAQjZ6n~rHz30!_D!QDm?BqU#wO^kNk%>X2WGWQNH*Hb*0u$=s3;RiZwtxUZ!W@SH diff --git a/web/public/theme-mode/custom-mode.svg b/web/public/theme-mode/custom-mode.svg deleted file mode 100644 index 01be18d5b40..00000000000 --- a/web/public/theme-mode/custom-mode.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/custom-theme-banner.svg b/web/public/theme-mode/custom-theme-banner.svg deleted file mode 100644 index b7434cd764e..00000000000 --- a/web/public/theme-mode/custom-theme-banner.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-high-contrast.svg b/web/public/theme-mode/dark-high-contrast.svg deleted file mode 100644 index e5839b28600..00000000000 --- a/web/public/theme-mode/dark-high-contrast.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-mode.svg b/web/public/theme-mode/dark-mode.svg deleted file mode 100644 index b8c14711c17..00000000000 --- a/web/public/theme-mode/dark-mode.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-high-contrast.svg b/web/public/theme-mode/light-high-contrast.svg deleted file mode 100644 index c2f5ded22be..00000000000 --- a/web/public/theme-mode/light-high-contrast.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-mode.svg b/web/public/theme-mode/light-mode.svg deleted file mode 100644 index 9c7fdae4b8d..00000000000 --- a/web/public/theme-mode/light-mode.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/web-view-spinner.png b/web/public/web-view-spinner.png deleted file mode 100644 index 527f307c29f4f3ce814f7768090612a65f402123..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1456 zcmV;h1yA~kP)q;0MLRchA~g_{S)1UAU?!6ZxcvuWGI##3+c;HKUDs#?A%a9Ko6Vkfbaa$+xm=Qc z5^HN~g{P;d?>J^e=ZGp8LX*(~P$vb#GV=(KQgMjbm+I>3sw6T`l%RoTRxY5VClzh8 z#`*O&HZ}~Axk3aJiG%^={^Po2s2E{_fLZB*B(5tZIMq{-kcoH|h5X1irOI`c*4NiJ znIMD-LPZlQCb%vMfzy&C9wg6SJJx848iM(JKCrX1Q=o=(iPe+TaL~Cf1*VqV7GGIe zsRUPrtOnoHo#W=`=i{@pv!`=&bA_e^ySuw9krwIc=_xlN>QErS1&rxj-0%1EH#D$* zjd}|U3o?jNa?fHNneGd+|F{xt03ur4-Q7)+y3_#`X&XD_#whfI8{}+~CeG9FJ(<-8 zMh-4vJp@RFpqBRj{!Y0y#NQBo-}vjgy?A?jJB3M<5}X;|KuawyE^d-pF{WsbDItxF zIoF}og6O3&5){&5+A#Hl7%84&^|038+uPf0N{|4>Xi*A)MyaHP8;4EPw2A{Lsxuc5 z$;ZdX5cwUchOWF*F&pXU=jZI<;bEPdG_7W!W(W(CA4z;a$mf=pmQ>~e7Smp;#snqr(;K=2#;8fY)ifMjuEVnS{^hQO3j+UOBoYc$Z zGWlbrn$ZUa2CPa2{g%qXV9+FTRPn&1^d@K(7=~e(P)(tS$A6MzeTv+))9f`SMn$1E zAZMU23Swa*M;&;G6#Hp%OOgBgd!?_h?>k!g?(U8rfQVURNHr*E6&D~tbs&{zImdm- zzcVnoaKTE2rv_3%OsW>BH#avjOjZ4IK$VSsTImbG1!Rn)f}q5Wi`yn=Fr|5MaiQWN zg1BtDFFQ+qk~cv&MGJ#iU8FkR=FEqO2iHi%*aUYTuxrdJ$mc**0Z~#^LsJl#HJgT~ zuUogbw@Lc4z;!9`KT83a_>}z&s#t%2zeXp+!@~vMZJ>VS>gr0k8WMi(q`&|M?gRf*B}o#lD}@PSVR}!ID%T(@l&C3kIX@-Hco9>WAcP3o>hUGMHaIw_ zGeKYvCqFf~uB51nLIj-!Celorc>t-1Lv;=X4AM^f2-<3qndl=@Vsv!WU> Date: Fri, 8 Mar 2024 17:36:28 +0530 Subject: [PATCH 049/214] [WEB-665] fix Padding in the project dropdown. (#3905) --- web/components/project/sidebar-list.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 2cee91e6bd3..5744ee331a5 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -116,9 +116,14 @@ export const ProjectSidebarList: FC = observer(() => { )}
From f5151ae717de6d96a3bd2b7c4d68e5dc8cc97506 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:37:01 +0530 Subject: [PATCH 050/214] [WEB-666] chore: rename `View profile` to `My activity`. (#3906) --- web/components/workspace/sidebar-dropdown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 5d1695b3368..5d07ff0f910 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -24,8 +24,8 @@ const userLinks = (workspaceSlug: string, userId: string) => [ icon: Mails, }, { - key: "view_profile", - name: "View profile", + key: "my_activity", + name: "My activity", href: `/${workspaceSlug}/profile/${userId}`, icon: CircleUserRound, }, @@ -38,7 +38,7 @@ const userLinks = (workspaceSlug: string, userId: string) => [ ]; const profileLinks = (workspaceSlug: string, userId: string) => [ { - name: "View profile", + name: "My activity", icon: UserCircle2, link: `/${workspaceSlug}/profile/${userId}`, }, From 2074bb97dbed9a55a65275cab0708e00deed013d Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:38:42 +0530 Subject: [PATCH 051/214] [WEB-440] feat: create project feature selection modal. (#3909) * [WEB-440] feat: create project feature selection modal. * [WEB-399] chore: explain project identifier. * chore: use `Link` component for redirection to project page. --- .../project/create-project-form.tsx | 392 +++++++++++++++++ .../project/create-project-modal.tsx | 399 ++---------------- web/components/project/index.ts | 2 + .../project/project-feature-update.tsx | 57 +++ .../project/settings/features-list.tsx | 69 +-- .../[projectId]/settings/features.tsx | 12 +- 6 files changed, 535 insertions(+), 396 deletions(-) create mode 100644 web/components/project/create-project-form.tsx create mode 100644 web/components/project/project-feature-update.tsx diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx new file mode 100644 index 00000000000..509cf310c0f --- /dev/null +++ b/web/components/project/create-project-form.tsx @@ -0,0 +1,392 @@ +import { useState, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; +import { useForm, Controller } from "react-hook-form"; +import { Info, X } from "lucide-react"; +// ui +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, + Tooltip, +} from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; +import { MemberDropdown } from "components/dropdowns"; +import { ProjectLogo } from "./project-logo"; +// constants +import { PROJECT_CREATED } from "constants/event-tracker"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +// helpers +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; +import { cn } from "helpers/common.helper"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; +// types +import { IProject } from "@plane/types"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; +}; + +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + +export const CreateProjectForm: FC = observer((props) => { + const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; + // store + const { captureProjectEvent } = useEventTracker(); + const { addProjectToFavorites, createProject } = useProject(); + // states + const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); + // form info + const { + formState: { errors, isSubmitting }, + handleSubmit, + reset, + control, + watch, + setValue, + } = useForm({ + defaultValues, + reValidateMode: "onChange", + }); + + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: Partial) => { + // Upper case identifier + formData.identifier = formData.identifier?.toUpperCase(); + + return createProject(workspaceSlug.toString(), formData) + .then((res) => { + const newPayload = { + ...res, + state: "SUCCESS", + }; + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: newPayload, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Project created successfully.", + }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } + handleNextStep(res.id); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: { + ...formData, + state: "FAILED", + }, + }); + }); + }); + }; + + const handleNameChange = (onChange: any) => (e: ChangeEvent) => { + if (!isChangeInIdentifierRequired) { + onChange(e); + return; + } + if (e.target.value === "") setValue("identifier", ""); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); + onChange(e); + }; + + const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { + const { value } = e.target; + const alphanumericValue = projectIdentifierSanitizer(value); + setIsChangeInIdentifierRequired(false); + onChange(alphanumericValue); + }; + + const handleClose = () => { + onClose(); + setIsChangeInIdentifierRequired(true); + setTimeout(() => { + reset(); + }, 300); + }; + + return ( + <> +
+ {watch("cover_image") && ( + Cover image + )} + +
+ +
+
+ ( + + )} + /> +
+
+ ( + + + + } + onChange={(val: any) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + /> + )} + /> +
+
+
+
+
+
+ ( + + )} + /> + + <>{errors?.name?.message} + +
+
+ + /^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || + "Only Alphanumeric & Non-latin characters are allowed.", + minLength: { + value: 1, + message: "Project ID must at least be of 1 character", + }, + maxLength: { + value: 5, + message: "Project ID must at most be of 5 characters", + }, + }} + render={({ field: { value, onChange } }) => ( + + )} + /> + + + + {errors?.identifier?.message} +
+
+ ( +