[WEB-4300] improvement: add allowedProjectIds to create work item modal (#7195)

This commit is contained in:
Prateek Shourya
2025-06-10 20:32:39 +05:30
committed by GitHub
parent 32d5fea3d3
commit 9c28db8b7b
10 changed files with 72 additions and 55 deletions

View File

@@ -39,6 +39,7 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
toggleDeleteIssueModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
createWorkItemAllowedProjectIds,
} = useCommandPalette();
// derived values
const issueDetails = issueId ? getIssueById(issueId) : undefined;
@@ -80,6 +81,7 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
data={getCreateIssueModalData()}
isDraft={isDraftIssue}
onSubmit={handleCreateIssueSubmit}
allowedProjectIds={createWorkItemAllowedProjectIds}
/>
{workspaceSlug && projectId && issueId && issueDetails && (
<DeleteIssueModal

View File

@@ -4,21 +4,29 @@ import { observer } from "mobx-react-lite";
import { ISearchIssueResponse, TIssue } from "@plane/types";
// components
import { IssueModalContext } from "@/components/issues";
// hooks
import { useUser } from "@/hooks/store/user/user-user";
export type TIssueModalProviderProps = {
templateId?: string;
dataForPreload?: Partial<TIssue>;
allowedProjectIds?: string[];
children: React.ReactNode;
};
export const IssueModalProvider = observer((props: TIssueModalProviderProps) => {
const { children } = props;
const { children, allowedProjectIds } = props;
// states
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
// store hooks
const { projectsWithCreatePermissions } = useUser();
// derived values
const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {});
return (
<IssueModalContext.Provider
value={{
allowedProjectIds: allowedProjectIds ?? projectIdsWithCreatePermissions,
workItemTemplateId: null,
setWorkItemTemplateId: () => {},
isApplyingTemplate: false,

View File

@@ -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<ICycle>, dirtyFields: any) => Promise<void>;
@@ -36,7 +37,10 @@ const defaultValues: Partial<ICycle> = {
export const CycleForm: React.FC<Props> = (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> = (props) => {
<ProjectDropdown
value={value}
onChange={(val) => {
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")}
/>
</div>

View File

@@ -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<IssuesModalProps> = 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<IssuesModalProps> = 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<IssuesModalProps> = 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<IssuesModalProps> = 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,

View File

@@ -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<TIssueProjectSelectProps> = 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<TIssueProjectSelectProps> = observer((
rules={{
required: true,
}}
render={({ field: { value, onChange } }) =>
projectsWithCreatePermissions && projectsWithCreatePermissions[value!] ? (
<div className="h-7">
<ProjectDropdown
value={value}
onChange={(projectId) => {
onChange(projectId);
handleFormChange();
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>
</div>
) : (
<></>
)
}
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectDropdown
value={value}
onChange={(projectId) => {
onChange(projectId);
handleFormChange();
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => allowedProjectIds.includes(project.id)}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>
</div>
)}
/>
);
});

View File

@@ -47,6 +47,7 @@ export type THandleParentWorkItemDetailsProps = {
};
export type TIssueModalContext = {
allowedProjectIds: string[];
workItemTemplateId: string | null;
setWorkItemTemplateId: React.Dispatch<React.SetStateAction<string | null>>;
isApplyingTemplate: boolean;

View File

@@ -29,6 +29,7 @@ export interface IssuesModalProps {
};
isProjectSelectionDisabled?: boolean;
templateId?: string;
allowedProjectIds?: string[];
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
@@ -43,7 +44,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
if (!props.isOpen) return null;
return (
<IssueModalProvider templateId={props.templateId} dataForPreload={dataForPreload}>
<IssueModalProvider
templateId={props.templateId}
dataForPreload={dataForPreload}
allowedProjectIds={props.allowedProjectIds}
>
<CreateUpdateIssueModalBase {...props} />
</IssueModalProvider>
);

View File

@@ -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<IModule>, dirtyFields: any) => Promise<void>;
@@ -37,6 +37,8 @@ const defaultValues: Partial<IModule> = {
export const ModuleForm: React.FC<Props> = (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> = (props) => {
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]}
tabIndex={getIndex("cover_image")}
/>
</div>

View File

@@ -26,6 +26,7 @@ export interface IBaseCommandPaletteStore {
isDeleteIssueModalOpen: boolean;
isBulkDeleteIssueModalOpen: boolean;
createIssueStoreType: TCreateModalStoreTypes;
createWorkItemAllowedProjectIds: string[] | undefined;
allStickiesModal: boolean;
projectListOpenMap: Record<string, boolean>;
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<string, boolean> = {};
@@ -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;
}
};

View File

@@ -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