diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 74d81a83eb7..f9ca2b61e12 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -2,6 +2,27 @@ name: Branch Build on: workflow_dispatch: + inputs: + build-web: + required: false + description: "Build Web" + type: boolean + default: false + build-space: + required: false + description: "Build Space" + type: boolean + default: false + build-api: + required: false + description: "Build API" + type: boolean + default: false + build-proxy: + required: false + description: "Build Proxy" + type: boolean + default: false push: branches: - master @@ -18,7 +39,7 @@ jobs: name: Build-Push Web/Space/API/Proxy Docker Image runs-on: ubuntu-latest outputs: - gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }} + gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }} gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }} gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }} gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }} @@ -74,7 +95,7 @@ jobs: - nginx/** branch_build_push_frontend: - if: ${{ (needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master') && !contains(needs.branch_build_setup.outputs.gh_buildx_platforms, 'linux/arm64') }} + if: ${{ (needs.branch_build_setup.outputs.build_frontend == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master') && !contains(needs.branch_build_setup.outputs.gh_buildx_platforms, 'linux/arm64') }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -298,7 +319,7 @@ jobs: ${{ env.FRONTEND_TAG_ARM64 }} branch_build_push_space: - if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event.inputs.build-space=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -351,7 +372,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_backend: - if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event.inputs.build-api=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -404,7 +425,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_proxy: - if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -455,4 +476,3 @@ jobs: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 12549cff514..c5eec3cd3ad 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 }} @@ -181,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) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 6f9b2618e19..35d997d1544 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -88,7 +88,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter( - Q(project_projectmember__member=self.request.user) + Q( + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + ) | Q(network=2) ) .select_related( @@ -173,10 +176,7 @@ def list(self, request, slug): for field in request.GET.get("fields", "").split(",") if field ] - projects = ( - self.get_queryset() - .order_by("sort_order", "name") - ) + projects = self.get_queryset().order_by("sort_order", "name") if request.GET.get("per_page", False) and request.GET.get( "cursor", False ): @@ -576,9 +576,11 @@ def post(self, request, slug, project_id, pk): _ = WorkspaceMember.objects.create( workspace_id=project_invite.workspace_id, member=user, - role=15 - if project_invite.role >= 15 - else project_invite.role, + role=( + 15 + if project_invite.role >= 15 + else project_invite.role + ), ) else: # Else make him active @@ -685,9 +687,14 @@ def create(self, request, slug, project_id): ) bulk_project_members = [] - member_roles = {member.get("member_id"): member.get("role") for member in members} + member_roles = { + member.get("member_id"): member.get("role") for member in members + } # Update roles in the members array based on the member_roles dictionary - for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]): + for project_member in ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ): project_member.role = member_roles[str(project_member.member_id)] project_member.is_active = True bulk_project_members.append(project_member) @@ -710,9 +717,9 @@ def create(self, request, slug, project_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, + sort_order=( + sort_order[0] - 10000 if len(sort_order) else 65535 + ), ) ) bulk_issue_props.append( @@ -733,7 +740,10 @@ def create(self, request, slug, project_id): bulk_issue_props, batch_size=10, ignore_conflicts=True ) - project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]) + project_members = ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ) serializer = ProjectMemberRoleSerializer(project_members, many=True) return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 88ea66c4c8e..9ed2323de2d 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -1,78 +1,79 @@ -# 1-Click Self-Hosting +# One-click deploy -In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. +Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane. -Let's get started! +This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you. -## Installing Plane +### Requirements -Installing Plane is a very easy and minimal step process. +- Operating systems: Debian, Ubuntu, CentOS +- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64 -### Prerequisite +### Download the latest stable release -- Operating System (latest): Debian / Ubuntu / Centos -- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64 - -### Downloading Latest Stable Release +Run ↓ on any CLI. ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - - ``` - - Downloading Preview Release +### Download the Preview release + +`Preview` builds do not support ARM64, AArch64 CPU architectures + +Run ↓ on any CLI. ``` export BRANCH=preview - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - - ``` -NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture - - --- +--- +### Successful installation -Expect this after a successful install +You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance. ![Install Output](images/install.png) -Access the application on a browser via http://server-ip-address - --- -### Get Control of your Plane Server Setup +### Manage your Plane instance -Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane +Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`. ![Plane Help](images/help.png) -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) - -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. 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`. - -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. Basic operators + + 1. `plane-app start` starts the Plane server. + 2. `plane-app restart` restarts the Plane server. + 3. `plane-app stop` stops the Plane server. + +2. Advanced operators + + `plane-app --configure` will show advanced configurators. + + - Change your proxy or listening port + Default: 80 + - Change your domain name + Default: Deployed server's public IP address + - File upload size + Default: 5MB + - Specify external database address when using an external database + Default: `Empty` + `Default folder: /opt/plane/data/postgres` + - Specify external Redis URL when using external Redis + Default: `Empty` + `Default folder: /opt/plane/data/redis` + - Configure AWS S3 bucket + Use only when you or your users want to use S3 + `Default folder: /opt/plane/data/minio` + +3. Version operators + + 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images + 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. + 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in + Postgres, Redis, and Minio alone. + 4. `plane-app --install` installs the Plane app again. diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app index be3718c9267..2f6c8b73069 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 ✅" diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 590015e122c..deffd883afd 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -7,6 +7,8 @@ import useToast from "hooks/use-toast"; import { usePopper } from "react-popper"; // ui import { Button, Input } from "@plane/ui"; +// icons +import { AlertCircle } from "lucide-react"; // components import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor"; import { Popover, Transition } from "@headlessui/react"; @@ -250,8 +252,17 @@ export const GptAssistantPopover: React.FC = (props) => { /> )} /> - - {responseActionButton} + + {responseActionButton ? ( + <>{responseActionButton}> + ) : ( + <> + + + By using this feature, you consent to sharing the message with a 3rd party service. + + > + )} Close diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 3cbab589f13..5bb46f14339 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -58,6 +58,7 @@ export interface IGroupByKanBan { scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; showEmptyGroup?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } const GroupByKanBan: React.FC = observer((props) => { @@ -83,6 +84,7 @@ const GroupByKanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup = true, + subGroupIssueHeaderCount, } = props; const member = useMember(); @@ -97,17 +99,27 @@ const GroupByKanBan: React.FC = observer((props) => { if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); - - const groupList = showEmptyGroup ? list : groupWithIssues; - - const visibilityGroupBy = (_list: IGroupByColumn) => { + const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { if (sub_group_by) { - if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true; + } + return groupVisibility; } else { - if (kanbanFilters?.group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true; + else groupVisibility.showGroup = false; + } + if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false; + return groupVisibility; } }; @@ -115,13 +127,18 @@ const GroupByKanBan: React.FC = observer((props) => { return ( - {groupList && - groupList.length > 0 && - groupList.map((_list: IGroupByColumn) => { + {list && + list.length > 0 && + list.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); + if (groupByVisibilityToggle.showGroup === false) return <>>; return ( - + {sub_group_by === null && ( = observer((props) => { )} - {!groupByVisibilityToggle && ( + {groupByVisibilityToggle.showIssues && ( = observer((props) => { viewId={viewId} disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} - groupByVisibilityToggle={groupByVisibilityToggle} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} /> @@ -197,6 +213,7 @@ export interface IKanBan { canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } export const KanBan: React.FC = observer((props) => { @@ -221,6 +238,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup, + subGroupIssueHeaderCount, } = props; const issueKanBanView = useKanbanView(); @@ -248,6 +266,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} showEmptyGroup={showEmptyGroup} + subGroupIssueHeaderCount={subGroupIssueHeaderCount} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index a05fb179110..5a46324bd2c 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -37,7 +37,7 @@ interface IKanbanGroup { viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; - groupByVisibilityToggle: boolean; + groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; } diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index d60e3b61868..11f5304b9f5 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader { list: IGroupByColumn[]; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; + showEmptyGroup: boolean; } const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { @@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st return headerCount; }; +const visibilitySubGroupByGroupCount = ( + issueIds: TSubGroupedIssues, + _list: IGroupByColumn, + showEmptyGroup: boolean +): boolean => { + let subGroupHeaderVisibility = true; + + if (showEmptyGroup) subGroupHeaderVisibility = true; + else { + if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true; + else subGroupHeaderVisibility = false; + } + + return subGroupHeaderVisibility; +}; + const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, @@ -46,25 +63,36 @@ const SubGroupSwimlaneHeader: React.FC = ({ list, kanbanFilters, handleKanbanFilters, + showEmptyGroup, }) => ( {list && list.length > 0 && - list.map((_list: IGroupByColumn) => ( - - - - ))} + list.map((_list: IGroupByColumn) => { + const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount( + issueIds as TSubGroupedIssues, + _list, + showEmptyGroup + ); + + if (subGroupByVisibilityToggle === false) return <>>; + + return ( + + + + ); + })} ); @@ -124,52 +152,74 @@ const SubGroupSwimlane: React.FC = observer((props) => { return issueCount; }; + const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { + const subGroupVisibility = { + showGroup: true, + showIssues: true, + }; + if (showEmptyGroup) subGroupVisibility.showGroup = true; + else { + if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true; + else subGroupVisibility.showGroup = false; + } + if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false; + return subGroupVisibility; + }; + return ( {list && list.length > 0 && - list.map((_list: any) => ( - - - - - - - + list.map((_list: any) => { + const subGroupByVisibilityToggle = visibilitySubGroupBy(_list); + if (subGroupByVisibilityToggle.showGroup === false) return <>>; - {!kanbanFilters?.sub_group_by.includes(_list.id) && ( - - + return ( + + + + + + - )} - - ))} + + {subGroupByVisibilityToggle.showIssues && ( + + + getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) + } + /> + + )} + + ); + })} ); }); @@ -261,6 +311,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} list={groupByList} + showEmptyGroup={showEmptyGroup} /> diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index a267ac9c81a..563fe9bb57e 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -1,16 +1,17 @@ -import orderBy from "lodash/orderBy"; import get from "lodash/get"; import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; import values from "lodash/values"; -// types -import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; -import { IIssueRootStore } from "../root.store"; // constants import { ISSUE_PRIORITIES } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; +// store +import { IIssueRootStore } from "../root.store"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; @@ -62,35 +63,35 @@ export class IssueHelperStore implements TIssueHelperStore { issues: TIssueMap, isCalendarIssues: boolean = false ) => { - const _issues: { [group_id: string]: string[] } = {}; - if (!groupBy) return _issues; + const currentIssues: { [group_id: string]: string[] } = {}; + if (!groupBy) return currentIssues; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { - _issues[group] = []; + currentIssues[group] = []; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let groupArray = []; if (groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; groupArray = [state_group]; } else { - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : []; + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"]; } for (const group of groupArray) { - if (group && _issues[group]) _issues[group].push(_issue.id); - else if (group) _issues[group] = [_issue.id]; + if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id); + else if (group) currentIssues[group] = [currentIssue.id]; } } - return _issues; + return currentIssues; }; subGroupedIssues = ( @@ -99,45 +100,47 @@ export class IssueHelperStore implements TIssueHelperStore { orderBy: TIssueOrderByOptions, issues: TIssueMap ) => { - const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; - if (!subGroupBy || !groupBy) return _issues; + const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; + if (!subGroupBy || !groupBy) return currentIssues; - this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => { + this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => { const groupByIssues: { [group_id: string]: string[] } = {}; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { groupByIssues[group] = []; }); - _issues[sub_group] = groupByIssues; + currentIssues[sub_group] = groupByIssues; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let subGroupArray = []; let groupArray = []; if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; + subGroupArray = [state_group]; groupArray = [state_group]; } else { - const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : []; - groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : []; + const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + + subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"]; + groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"]; } for (const subGroup of subGroupArray) { for (const group of groupArray) { - if (subGroup && group && _issues?.[subGroup]?.[group]) _issues[subGroup][group].push(_issue.id); - else if (subGroup && group && _issues[subGroup]) _issues[subGroup][group] = [_issue.id]; - else if (subGroup && group) _issues[subGroup] = { [group]: [_issue.id] }; + if (subGroup && group && currentIssues?.[subGroup]?.[group]) + currentIssues[subGroup][group].push(currentIssue.id); + else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group] = [currentIssue.id]; + else if (subGroup && group) currentIssues[subGroup] = { [group]: [currentIssue.id] }; } } } - return _issues; + return currentIssues; }; unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) => @@ -215,8 +218,8 @@ export class IssueHelperStore implements TIssueHelperStore { const moduleMap = this.rootStore?.moduleMap; if (!moduleMap) break; for (const dataId of dataIdsArray) { - const _module = moduleMap[dataId]; - if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase()); + const currentModule = moduleMap[dataId]; + if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase()); } break; case "cycle_id": @@ -233,10 +236,10 @@ export class IssueHelperStore implements TIssueHelperStore { } /** - * This Method is mainly used to filter out empty values in the begining + * This Method is mainly used to filter out empty values in the beginning * @param key key of the value that is to be checked if empty * @param object any object in which the key's value is to be checked - * @returns 1 if emoty, 0 if not empty + * @returns 1 if empty, 0 if not empty */ getSortOrderToFilterEmptyValues(key: string, object: any) { const value = object?.[key]; @@ -388,7 +391,7 @@ export class IssueHelperStore implements TIssueHelperStore { getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] { if (!value || value === null || value === undefined) return ["None"]; if (Array.isArray(value)) - if (value.length) return value; + if (value && value.length) return value; else return ["None"]; else if (typeof value === "boolean") return [value ? "True" : "False"]; else if (typeof value === "number") return [value.toString()];
By using this feature, you consent to sharing the message with a 3rd party service.