From 9c28db8b7b58cafd200789948bfc3ceb6a93d9f5 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 10 Jun 2025 20:32:39 +0530 Subject: [PATCH] [WEB-4300] improvement: add allowedProjectIds to create work item modal (#7195) --- .../command-palette/modals/issue-level.tsx | 2 + .../issues/issue-modal/provider.tsx | 10 ++++- web/core/components/cycles/form.tsx | 14 ++++-- .../components/issues/issue-modal/base.tsx | 20 +++++---- .../issue-modal/components/project-select.tsx | 44 +++++++++---------- .../context/issue-modal-context.tsx | 1 + .../components/issues/issue-modal/modal.tsx | 7 ++- web/core/components/modules/form.tsx | 8 ++-- web/core/store/base-command-palette.store.ts | 9 +++- web/helpers/project.helper.ts | 12 +---- 10 files changed, 72 insertions(+), 55 deletions(-) diff --git a/web/ce/components/command-palette/modals/issue-level.tsx b/web/ce/components/command-palette/modals/issue-level.tsx index 02eada5718..f88908f253 100644 --- a/web/ce/components/command-palette/modals/issue-level.tsx +++ b/web/ce/components/command-palette/modals/issue-level.tsx @@ -39,6 +39,7 @@ export const IssueLevelModals: FC = observer((props) => toggleDeleteIssueModal, isBulkDeleteIssueModalOpen, toggleBulkDeleteIssueModal, + createWorkItemAllowedProjectIds, } = useCommandPalette(); // derived values const issueDetails = issueId ? getIssueById(issueId) : undefined; @@ -80,6 +81,7 @@ export const IssueLevelModals: FC = observer((props) => data={getCreateIssueModalData()} isDraft={isDraftIssue} onSubmit={handleCreateIssueSubmit} + allowedProjectIds={createWorkItemAllowedProjectIds} /> {workspaceSlug && projectId && issueId && issueDetails && ( ; + allowedProjectIds?: string[]; children: React.ReactNode; }; export const IssueModalProvider = observer((props: TIssueModalProviderProps) => { - const { children } = props; + const { children, allowedProjectIds } = props; // states const [selectedParentIssue, setSelectedParentIssue] = useState(null); + // store hooks + const { projectsWithCreatePermissions } = useUser(); + // derived values + const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {}); return ( {}, isApplyingTemplate: false, diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx index c5c9c91869..601ae557cd 100644 --- a/web/core/components/cycles/form.tsx +++ b/web/core/components/cycles/form.tsx @@ -14,8 +14,9 @@ import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns"; // constants // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; +// hooks +import { useUser } from "@/hooks/store/user/user-user"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -36,7 +37,10 @@ const defaultValues: Partial = { export const CycleForm: React.FC = (props) => { const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props; + // plane hooks const { t } = useTranslation(); + // store hooks + const { projectsWithCreatePermissions } = useUser(); // form data const { formState: { errors, isSubmitting, dirtyFields }, @@ -75,12 +79,14 @@ export const CycleForm: React.FC = (props) => { { - onChange(val); - setActiveProject(val); + if (!Array.isArray(val)) { + onChange(val); + setActiveProject(val); + } }} multiple={false} buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} + renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]} tabIndex={getIndex("cover_image")} /> diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index d272232393..80370100fd 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -13,7 +13,12 @@ import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issu // constants // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; -import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser, useProject } from "@/hooks/store"; +import { useCycle } from "@/hooks/store/use-cycle"; +import { useEventTracker } from "@/hooks/store/use-event-tracker"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; +import { useIssues } from "@/hooks/store/use-issues"; +import { useModule } from "@/hooks/store/use-module"; +import { useProject } from "@/hooks/store/use-project"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; // services @@ -59,14 +64,13 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { t } = useTranslation(); const { captureIssueEvent } = useEventTracker(); const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId, workItem } = useParams(); - const { projectsWithCreatePermissions } = useUser(); const { fetchCycleDetails } = useCycle(); const { fetchModuleDetails } = useModule(); const { issues } = useIssues(storeType); const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); const { fetchIssue } = useIssueDetail(); - const { handleCreateUpdatePropertyValues } = useIssueModal(); + const { allowedProjectIds, handleCreateUpdatePropertyValues } = useIssueModal(); const { getProjectByIdentifier } = useProject(); // pathname const pathname = usePathname(); @@ -76,7 +80,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const routerProjectIdentifier = workItem?.toString().split("-")[0]; const projectIdFromRouter = getProjectByIdentifier(routerProjectIdentifier)?.id; const projectId = data?.project_id ?? routerProjectId?.toString() ?? projectIdFromRouter; - const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {}); const fetchIssueDetail = async (issueId: string | undefined) => { setDescription(undefined); @@ -114,10 +117,9 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( return; } - // if data is not present, set active project to the project - // in the url. This has the least priority. - if (projectIdsWithCreatePermissions && projectIdsWithCreatePermissions.length > 0 && !activeProjectId) - setActiveProjectId(projectId?.toString() ?? projectIdsWithCreatePermissions?.[0]); + // if data is not present, set active project to the first project in the allowedProjectIds array + if (allowedProjectIds && allowedProjectIds.length > 0 && !activeProjectId) + setActiveProjectId(projectId?.toString() ?? allowedProjectIds?.[0]); // clearing up the description state when we leave the component return () => setDescription(undefined); @@ -346,7 +348,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value); // don't open the modal if there are no projects - if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null; + if (!allowedProjectIds || allowedProjectIds.length === 0 || !activeProjectId) return null; const commonIssueModalProps: IssueFormProps = { issueTitleRef: issueTitleRef, diff --git a/web/core/components/issues/issue-modal/components/project-select.tsx b/web/core/components/issues/issue-modal/components/project-select.tsx index 29268b4adc..3c2d235d09 100644 --- a/web/core/components/issues/issue-modal/components/project-select.tsx +++ b/web/core/components/issues/issue-modal/components/project-select.tsx @@ -10,10 +10,9 @@ import { TIssue } from "@plane/types"; // components import { ProjectDropdown } from "@/components/dropdowns"; // helpers -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; -// store hooks -import { useUser } from "@/hooks/store"; +// hooks +import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { usePlatformOS } from "@/hooks/use-platform-os"; type TIssueProjectSelectProps = { @@ -25,8 +24,9 @@ type TIssueProjectSelectProps = { export const IssueProjectSelect: React.FC = observer((props) => { const { control, disabled = false, handleFormChange } = props; // store hooks - const { projectsWithCreatePermissions } = useUser(); const { isMobile } = usePlatformOS(); + // context hooks + const { allowedProjectIds } = useIssueModal(); const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile); @@ -37,26 +37,22 @@ export const IssueProjectSelect: React.FC = observer(( rules={{ required: true, }} - render={({ field: { value, onChange } }) => - projectsWithCreatePermissions && projectsWithCreatePermissions[value!] ? ( -
- { - onChange(projectId); - handleFormChange(); - }} - multiple={false} - buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} - tabIndex={getIndex("project_id")} - disabled={disabled} - /> -
- ) : ( - <> - ) - } + render={({ field: { value, onChange } }) => ( +
+ { + onChange(projectId); + handleFormChange(); + }} + multiple={false} + buttonVariant="border-with-text" + renderCondition={(project) => allowedProjectIds.includes(project.id)} + tabIndex={getIndex("project_id")} + disabled={disabled} + /> +
+ )} /> ); }); diff --git a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx index 59d6558a6f..0646408852 100644 --- a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx +++ b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx @@ -47,6 +47,7 @@ export type THandleParentWorkItemDetailsProps = { }; export type TIssueModalContext = { + allowedProjectIds: string[]; workItemTemplateId: string | null; setWorkItemTemplateId: React.Dispatch>; isApplyingTemplate: boolean; diff --git a/web/core/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/modal.tsx index 0ba526e1da..b87573808b 100644 --- a/web/core/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/modal.tsx @@ -29,6 +29,7 @@ export interface IssuesModalProps { }; isProjectSelectionDisabled?: boolean; templateId?: string; + allowedProjectIds?: string[]; } export const CreateUpdateIssueModal: React.FC = observer((props) => { @@ -43,7 +44,11 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (!props.isOpen) return null; return ( - + ); diff --git a/web/core/components/modules/form.tsx b/web/core/components/modules/form.tsx index 4123c0bb10..675850e7e5 100644 --- a/web/core/components/modules/form.tsx +++ b/web/core/components/modules/form.tsx @@ -13,9 +13,9 @@ import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "@/components import { ModuleStatusSelect } from "@/components/modules"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; -// types +// hooks +import { useUser } from "@/hooks/store/user/user-user"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -37,6 +37,8 @@ const defaultValues: Partial = { export const ModuleForm: React.FC = (props) => { const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props; + // store hooks + const { projectsWithCreatePermissions } = useUser(); // form info const { formState: { errors, isSubmitting, dirtyFields }, @@ -93,7 +95,7 @@ export const ModuleForm: React.FC = (props) => { }} multiple={false} buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} + renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]} tabIndex={getIndex("cover_image")} /> diff --git a/web/core/store/base-command-palette.store.ts b/web/core/store/base-command-palette.store.ts index 7024daf4da..9c7273a5f9 100644 --- a/web/core/store/base-command-palette.store.ts +++ b/web/core/store/base-command-palette.store.ts @@ -26,6 +26,7 @@ export interface IBaseCommandPaletteStore { isDeleteIssueModalOpen: boolean; isBulkDeleteIssueModalOpen: boolean; createIssueStoreType: TCreateModalStoreTypes; + createWorkItemAllowedProjectIds: string[] | undefined; allStickiesModal: boolean; projectListOpenMap: Record; getIsProjectListOpen: (projectId: string) => boolean; @@ -36,7 +37,7 @@ export interface IBaseCommandPaletteStore { toggleCreateCycleModal: (value?: boolean) => void; toggleCreateViewModal: (value?: boolean) => void; toggleCreatePageModal: (value?: TCreatePageModal) => void; - toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes) => void; + toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => void; toggleCreateModuleModal: (value?: boolean) => void; toggleDeleteIssueModal: (value?: boolean) => void; toggleBulkDeleteIssueModal: (value?: boolean) => void; @@ -57,6 +58,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor isBulkDeleteIssueModalOpen: boolean = false; createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; + createWorkItemAllowedProjectIds: IBaseCommandPaletteStore["createWorkItemAllowedProjectIds"] = undefined; allStickiesModal: boolean = false; projectListOpenMap: Record = {}; @@ -74,6 +76,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor isBulkDeleteIssueModalOpen: observable.ref, createPageModal: observable, createIssueStoreType: observable, + createWorkItemAllowedProjectIds: observable, allStickiesModal: observable, projectListOpenMap: observable, // projectPages: computed, @@ -214,13 +217,15 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor * @param storeType * @returns */ - toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes) => { + toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => { if (value !== undefined) { this.isCreateIssueModalOpen = value; this.createIssueStoreType = storeType || EIssuesStoreType.PROJECT; + this.createWorkItemAllowedProjectIds = allowedProjectIds ?? undefined; } else { this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen; this.createIssueStoreType = EIssuesStoreType.PROJECT; + this.createWorkItemAllowedProjectIds = undefined; } }; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index e8bdb5514a..b620542840 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -1,12 +1,10 @@ import sortBy from "lodash/sortBy"; // types -import { EUserPermissions } from "@plane/constants"; import { TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; // helpers import { getDate } from "@/helpers/date-time.helper"; import { satisfiesDateFilter } from "@/helpers/filter.helper"; -// plane web constants -// types +// plane web imports import { TProject } from "@/plane-web/types"; /** @@ -49,14 +47,6 @@ export const orderJoinedProjects = ( export const projectIdentifierSanitizer = (identifier: string): string => identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); -/** - * @description Checks if the project should be rendered or not based on the user role - * @param {TProject} project - * @returns {boolean} - */ -export const shouldRenderProject = (project: TProject): boolean => - !!project.member_role && project.member_role >= EUserPermissions.MEMBER; - /** * @description filters projects based on the filter * @param {TProject} project