[WEB-2848] improvement: enhanced components modularity (#6196)

* improvement: enhanced componenets modularity

* fix: lint errors resolved
This commit is contained in:
Prateek Shourya 2024-12-13 14:26:26 +05:30 committed by GitHub
parent ab11e83535
commit 9234f21f26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 240 additions and 77 deletions

View file

@ -23,20 +23,23 @@ export type CalendarStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
| EIssuesStoreType.MODULE | EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW; | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
interface IBaseCalendarRoot { interface IBaseCalendarRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
addIssuesToView?: (issueIds: string[]) => Promise<any>; addIssuesToView?: (issueIds: string[]) => Promise<any>;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
viewId?: string | undefined; viewId?: string | undefined;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
} }
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const { QuickActions, addIssuesToView, isCompletedCycle = false, viewId } = props; const { QuickActions, addIssuesToView, isCompletedCycle = false, viewId, canEditPropertiesBasedOnProject } = props;
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug } = useParams();
// hooks // hooks
const storeType = useIssueStoreType() as CalendarStoreType; const storeType = useIssueStoreType() as CalendarStoreType;
@ -61,6 +64,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
EUserPermissionsLevel.PROJECT EUserPermissionsLevel.PROJECT
); );
const { enableInlineEditing } = issues?.viewFlags || {};
const displayFilters = issuesFilter.issueFilters?.displayFilters; const displayFilters = issuesFilter.issueFilters?.displayFilters;
const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues;
@ -69,9 +74,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const { startDate, endDate } = issueCalendarView.getStartAndEndDate(layout) ?? {}; const { startDate, endDate } = issueCalendarView.getStartAndEndDate(layout) ?? {};
useEffect(() => { useEffect(() => {
startDate && if (startDate && endDate && layout) {
endDate &&
layout &&
fetchIssues( fetchIssues(
"init-loader", "init-loader",
{ {
@ -83,21 +86,23 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
}, },
viewId viewId
); );
}
}, [fetchIssues, storeType, startDate, endDate, layout, viewId]); }, [fetchIssues, storeType, startDate, endDate, layout, viewId]);
const handleDragAndDrop = async ( const handleDragAndDrop = async (
issueId: string | undefined, issueId: string | undefined,
issueProjectId: string | undefined,
sourceDate: string | undefined, sourceDate: string | undefined,
destinationDate: string | undefined destinationDate: string | undefined
) => { ) => {
if (!issueId || !destinationDate || !sourceDate) return; if (!issueId || !destinationDate || !sourceDate || !issueProjectId) return;
await handleDragDrop( await handleDragDrop(
issueId, issueId,
sourceDate, sourceDate,
destinationDate, destinationDate,
workspaceSlug?.toString(), workspaceSlug?.toString(),
projectId?.toString(), issueProjectId,
updateIssue updateIssue
).catch((err) => { ).catch((err) => {
setToast({ setToast({
@ -125,6 +130,16 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
[issues?.getGroupIssueCount] [issues?.getGroupIssueCount]
); );
const canEditProperties = useCallback(
(projectId: string | undefined) => {
const isEditingAllowedBasedOnProject =
canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed;
return enableInlineEditing && isEditingAllowedBasedOnProject;
},
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
);
return ( return (
<> <>
<div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4"> <div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4">
@ -145,7 +160,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)}
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={!canEditProperties(issue.project_id ?? undefined) || isCompletedCycle}
placements={placement} placements={placement}
/> />
)} )}
@ -154,9 +169,10 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
getGroupIssueCount={getGroupIssueCount} getGroupIssueCount={getGroupIssueCount}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
quickAddCallback={quickAddIssue} quickAddCallback={quickAddIssue}
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={isCompletedCycle}
updateFilters={updateFilters} updateFilters={updateFilters}
handleDragAndDrop={handleDragAndDrop} handleDragAndDrop={handleDragAndDrop}
canEditProperties={canEditProperties}
/> />
</div> </div>
</> </>

View file

@ -26,9 +26,8 @@ import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constan
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks // hooks
import { useIssues, useUserPermissions } from "@/hooks/store"; import { useIssues } from "@/hooks/store";
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// store // store
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { ICalendarStore } from "@/store/issue/issue_calendar_view.store"; import { ICalendarStore } from "@/store/issue/issue_calendar_view.store";
@ -53,6 +52,7 @@ type Props = {
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
handleDragAndDrop: ( handleDragAndDrop: (
issueId: string | undefined, issueId: string | undefined,
issueProjectId: string | undefined,
sourceDate: string | undefined, sourceDate: string | undefined,
destinationDate: string | undefined destinationDate: string | undefined
) => Promise<void>; ) => Promise<void>;
@ -63,6 +63,7 @@ type Props = {
filterType: EIssueFilterType, filterType: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => Promise<void>; ) => Promise<void>;
canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarChart: React.FC<Props> = observer((props) => { export const CalendarChart: React.FC<Props> = observer((props) => {
@ -81,6 +82,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
getPaginationData, getPaginationData,
getGroupIssueCount, getGroupIssueCount,
updateFilters, updateFilters,
canEditProperties,
readOnly = false, readOnly = false,
} = props; } = props;
// states // states
@ -91,15 +93,10 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
const { const {
issues: { viewFlags }, issues: { viewFlags },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(EIssuesStoreType.PROJECT);
const { allowPermissions } = useUserPermissions();
const [windowWidth] = useSize(); const [windowWidth] = useSize();
const { enableIssueCreation } = viewFlags || {}; const { enableIssueCreation, enableQuickAdd } = viewFlags || {};
const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const calendarPayload = issueCalendarView.calendarPayload; const calendarPayload = issueCalendarView.calendarPayload;
@ -163,12 +160,13 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData} getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount} getGroupIssueCount={getGroupIssueCount}
enableQuickIssueCreate enableQuickIssueCreate={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties}
/> />
))} ))}
</div> </div>
@ -185,12 +183,13 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData} getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount} getGroupIssueCount={getGroupIssueCount}
enableQuickIssueCreate enableQuickIssueCreate={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties}
/> />
)} )}
</div> </div>
@ -209,11 +208,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
getPaginationData={getPaginationData} getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount} getGroupIssueCount={getGroupIssueCount}
quickActions={quickActions} quickActions={quickActions}
enableQuickIssueCreate enableQuickIssueCreate={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties}
isDragDisabled isDragDisabled
isMobileView isMobileView
/> />
@ -235,11 +235,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData} getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount} getGroupIssueCount={getGroupIssueCount}
enableQuickIssueCreate enableQuickIssueCreate={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties}
isDragDisabled isDragDisabled
isMobileView isMobileView
/> />

View file

@ -38,6 +38,7 @@ type Props = {
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
handleDragAndDrop: ( handleDragAndDrop: (
issueId: string | undefined, issueId: string | undefined,
issueProjectId: string | undefined,
sourceDate: string | undefined, sourceDate: string | undefined,
destinationDate: string | undefined destinationDate: string | undefined
) => Promise<void>; ) => Promise<void>;
@ -45,6 +46,7 @@ type Props = {
readOnly?: boolean; readOnly?: boolean;
selectedDate: Date; selectedDate: Date;
setSelectedDate: (date: Date) => void; setSelectedDate: (date: Date) => void;
canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarDayTile: React.FC<Props> = observer((props) => { export const CalendarDayTile: React.FC<Props> = observer((props) => {
@ -65,6 +67,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
selectedDate, selectedDate,
handleDragAndDrop, handleDragAndDrop,
setSelectedDate, setSelectedDate,
canEditProperties,
} = props; } = props;
const [isDraggingOver, setIsDraggingOver] = useState(false); const [isDraggingOver, setIsDraggingOver] = useState(false);
@ -111,7 +114,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
} }
} }
handleDragAndDrop(sourceData?.id, sourceData?.date, destinationData?.date); handleDragAndDrop(
sourceData?.id,
issueDetails?.project_id ?? undefined,
sourceData?.date,
destinationData?.date
);
highlightIssueOnDrop(source?.element?.id, false); highlightIssueOnDrop(source?.element?.id, false);
}, },
}) })
@ -176,6 +184,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties}
/> />
</div> </div>
</div> </div>

View file

@ -15,10 +15,11 @@ type Props = {
issueId: string; issueId: string;
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
isDragDisabled: boolean; isDragDisabled: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => { export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
const { issueId, quickActions, isDragDisabled } = props; const { issueId, quickActions, isDragDisabled, canEditProperties } = props;
const issueRef = useRef<HTMLAnchorElement | null>(null); const issueRef = useRef<HTMLAnchorElement | null>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@ -29,6 +30,8 @@ export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
const canDrag = !isDragDisabled && canEditProperties(issue?.project_id ?? undefined);
useEffect(() => { useEffect(() => {
const element = issueRef.current; const element = issueRef.current;
@ -37,7 +40,7 @@ export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
return combine( return combine(
draggable({ draggable({
element, element,
canDrag: () => !isDragDisabled, canDrag: () => canDrag,
getInitialData: () => ({ id: issue?.id, date: issue?.target_date }), getInitialData: () => ({ id: issue?.id, date: issue?.target_date }),
onDragStart: () => { onDragStart: () => {
setIsDragging(true); setIsDragging(true);
@ -47,7 +50,7 @@ export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
}, },
}) })
); );
}, [issueRef?.current, issue]); }, [issueRef?.current, issue, canDrag]);
useOutsideClickDetector(issueRef, () => { useOutsideClickDetector(issueRef, () => {
issueRef?.current?.classList?.remove(HIGHLIGHT_CLASS); issueRef?.current?.classList?.remove(HIGHLIGHT_CLASS);

View file

@ -22,6 +22,7 @@ type Props = {
addIssuesToView?: (issueIds: string[]) => Promise<any>; addIssuesToView?: (issueIds: string[]) => Promise<any>;
readOnly?: boolean; readOnly?: boolean;
isMobileView?: boolean; isMobileView?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
@ -37,6 +38,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
addIssuesToView, addIssuesToView,
readOnly, readOnly,
isMobileView = false, isMobileView = false,
canEditProperties,
} = props; } = props;
const formattedDatePayload = renderFormattedPayloadDate(date); const formattedDatePayload = renderFormattedPayloadDate(date);
@ -63,6 +65,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
issueId={issueId} issueId={issueId}
quickActions={quickActions} quickActions={quickActions}
isDragDisabled={isDragDisabled || isMobileView} isDragDisabled={isDragDisabled || isMobileView}
canEditProperties={canEditProperties}
/> />
</div> </div>
))} ))}

View file

@ -1,7 +1,32 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// hooks // hooks
import { ProjectIssueQuickActions } from "@/components/issues"; import { ProjectIssueQuickActions } from "@/components/issues";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components // components
import { BaseCalendarRoot } from "../base-calendar-root"; import { BaseCalendarRoot } from "../base-calendar-root";
export const CalendarLayout: React.FC = observer(() => <BaseCalendarRoot QuickActions={ProjectIssueQuickActions} />); export const CalendarLayout: React.FC = observer(() => {
// router
const { workspaceSlug } = useParams();
// hooks
const { allowPermissions } = useUserPermissions();
// derived values
const canEditPropertiesBasedOnProject = (projectId: string) =>
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
projectId
);
return (
<BaseCalendarRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
/>
);
});

View file

@ -26,6 +26,7 @@ type Props = {
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>; quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
handleDragAndDrop: ( handleDragAndDrop: (
issueId: string | undefined, issueId: string | undefined,
issueProjectId: string | undefined,
sourceDate: string | undefined, sourceDate: string | undefined,
destinationDate: string | undefined destinationDate: string | undefined
) => Promise<void>; ) => Promise<void>;
@ -33,6 +34,7 @@ type Props = {
readOnly?: boolean; readOnly?: boolean;
selectedDate: Date; selectedDate: Date;
setSelectedDate: (date: Date) => void; setSelectedDate: (date: Date) => void;
canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarWeekDays: React.FC<Props> = observer((props) => { export const CalendarWeekDays: React.FC<Props> = observer((props) => {
@ -53,6 +55,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
readOnly = false, readOnly = false,
selectedDate, selectedDate,
setSelectedDate, setSelectedDate,
canEditProperties,
} = props; } = props;
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
@ -88,6 +91,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
handleDragAndDrop={handleDragAndDrop} handleDragAndDrop={handleDragAndDrop}
canEditProperties={canEditProperties}
/> />
); );
})} })}

View file

@ -134,6 +134,13 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
values={value} values={value}
/> />
)} )}
{filterKey === "team_project" && (
<AppliedProjectFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("team_project", val)}
values={value}
/>
)}
{isEditingAllowed && ( {isEditingAllowed && (
<button <button
type="button" type="button"

View file

@ -25,7 +25,7 @@ import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
// hooks // hooks
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components // plane web components
import { FilterIssueTypes } from "@/plane-web/components/issues"; import { FilterIssueTypes, FilterTeamProjects } from "@/plane-web/components/issues";
type Props = { type Props = {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;
@ -208,6 +208,18 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
/> />
</div> </div>
)} )}
{/* team project */}
{isFilterEnabled("team_project") && (
<div className="py-2">
<FilterTeamProjects
appliedFilters={filters.team_project ?? null}
handleUpdate={(val) => handleFiltersUpdate("team_project", val)}
searchQuery={filtersSearchQuery}
/>
</div>
)}
{/* issue type */} {/* issue type */}
{isDisplayFilterEnabled("type") && displayFilters && handleDisplayFiltersUpdate && ( {isDisplayFilterEnabled("type") && displayFilters && handleDisplayFiltersUpdate && (
<div className="py-2"> <div className="py-2">

View file

@ -32,7 +32,10 @@ export type KanbanStoreType =
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.DRAFT | EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE; | EIssuesStoreType.PROFILE
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
export interface IBaseKanBanLayout { export interface IBaseKanBanLayout {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
addIssuesToView?: (issueIds: string[]) => Promise<any>; addIssuesToView?: (issueIds: string[]) => Promise<any>;
@ -176,11 +179,11 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)}
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={!canEditProperties(issue.project_id ?? undefined) || isCompletedCycle}
/> />
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isCompletedCycle, canEditProperties, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {

View file

@ -35,6 +35,7 @@ interface IssueBlockProps {
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
draggableId: string; draggableId: string;
canDropOverIssue: boolean; canDropOverIssue: boolean;
canDragIssuesInCurrentGrouping: boolean;
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
@ -111,6 +112,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
issuesMap, issuesMap,
displayProperties, displayProperties,
canDropOverIssue, canDropOverIssue,
canDragIssuesInCurrentGrouping,
updateIssue, updateIssue,
quickActions, quickActions,
canEditProperties, canEditProperties,
@ -139,7 +141,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
const canEditIssueProperties = canEditProperties(issue?.project_id ?? undefined); const canEditIssueProperties = canEditProperties(issue?.project_id ?? undefined);
const isDragAllowed = !issue?.tempId && canEditIssueProperties; const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties;
useOutsideClickDetector(cardRef, () => { useOutsideClickDetector(cardRef, () => {
cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS); cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS);
@ -195,12 +197,15 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
className={cn("group/kanban-block relative mb-2", { "z-[1]": isCurrentBlockDragging })} className={cn("group/kanban-block relative mb-2", { "z-[1]": isCurrentBlockDragging })}
onDragStart={() => { onDragStart={() => {
if (isDragAllowed) setIsCurrentBlockDragging(true); if (isDragAllowed) setIsCurrentBlockDragging(true);
else else {
setToast({ setToast({
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING,
title: "Cannot move issue", title: "Cannot move issue",
message: "Drag and drop is disabled for the current grouping", message: !canEditIssueProperties
? "You are not allowed to move this issue"
: "Drag and drop is disabled for the current grouping",
}); });
}
}} }}
> >
<ControlLink <ControlLink

View file

@ -16,6 +16,7 @@ interface IssueBlocksListProps {
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
canDropOverIssue: boolean; canDropOverIssue: boolean;
canDragIssuesInCurrentGrouping: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
} }
@ -27,6 +28,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((p
issueIds, issueIds,
displayProperties, displayProperties,
canDropOverIssue, canDropOverIssue,
canDragIssuesInCurrentGrouping,
updateIssue, updateIssue,
quickActions, quickActions,
canEditProperties, canEditProperties,
@ -57,6 +59,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((p
quickActions={quickActions} quickActions={quickActions}
draggableId={draggableId} draggableId={draggableId}
canDropOverIssue={canDropOverIssue} canDropOverIssue={canDropOverIssue}
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
/> />

View file

@ -20,7 +20,7 @@ import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "@/components/issue
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
import { KanbanIssueBlockLoader } from "@/components/ui"; import { KanbanIssueBlockLoader } from "@/components/ui";
// helpers // helpers
import { EIssueLayoutTypes } from "@/constants/issue"; import { DRAG_ALLOWED_GROUPS, EIssueLayoutTypes } from "@/constants/issue";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
@ -258,6 +258,10 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults; const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults;
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || isDropDisabled; const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || isDropDisabled;
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible; const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const canDragIssuesInCurrentGrouping =
!!group_by &&
DRAG_ALLOWED_GROUPS.includes(group_by) &&
(!!sub_group_by ? DRAG_ALLOWED_GROUPS.includes(sub_group_by) : true);
return ( return (
<div <div
@ -289,6 +293,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
canDropOverIssue={!canOverlayBeVisible} canDropOverIssue={!canOverlayBeVisible}
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
/> />
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)} {shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}

View file

@ -1,9 +1,32 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// mobx store import { useParams } from "next/navigation";
import { ProjectIssueQuickActions } from "@/components/issues";
// components // components
// types import { ProjectIssueQuickActions } from "@/components/issues";
// constants // hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components
import { BaseKanBanRoot } from "../base-kanban-root"; import { BaseKanBanRoot } from "../base-kanban-root";
export const KanBanLayout: React.FC = observer(() => <BaseKanBanRoot QuickActions={ProjectIssueQuickActions} />); export const KanBanLayout: React.FC = observer(() => {
// router
const { workspaceSlug } = useParams();
// hooks
const { allowPermissions } = useUserPermissions();
// derived values
const canEditPropertiesBasedOnProject = (projectId: string) =>
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
projectId
);
return (
<BaseKanBanRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
/>
);
});

View file

@ -26,7 +26,10 @@ type ListStoreType =
| EIssuesStoreType.DRAFT | EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE | EIssuesStoreType.PROFILE
| EIssuesStoreType.ARCHIVED | EIssuesStoreType.ARCHIVED
| EIssuesStoreType.WORKSPACE_DRAFT; | EIssuesStoreType.WORKSPACE_DRAFT
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
interface IBaseListRoot { interface IBaseListRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
addIssuesToView?: (issueIds: string[]) => Promise<any>; addIssuesToView?: (issueIds: string[]) => Promise<any>;
@ -100,11 +103,11 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)}
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={!canEditProperties(issue.project_id ?? undefined) || isCompletedCycle}
/> />
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isCompletedCycle, canEditProperties, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
const loadMoreIssues = useCallback( const loadMoreIssues = useCallback(

View file

@ -85,8 +85,11 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
isArchived: !!issue.archived_at, isArchived: !!issue.archived_at,
}); });
// derived values
const issue = issuesMap[issueId]; const issue = issuesMap[issueId];
const subIssuesCount = issue?.sub_issues_count ?? 0; const subIssuesCount = issue?.sub_issues_count ?? 0;
const canEditIssueProperties = canEditProperties(issue?.project_id ?? undefined);
const isDraggingAllowed = canDrag && canEditIssueProperties;
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
@ -98,7 +101,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
return combine( return combine(
draggable({ draggable({
element, element,
canDrag: () => canDrag, canDrag: () => isDraggingAllowed,
getInitialData: () => ({ id: issueId, type: "ISSUE", groupId }), getInitialData: () => ({ id: issueId, type: "ISSUE", groupId }),
onDragStart: () => { onDragStart: () => {
setIsCurrentBlockDragging(true); setIsCurrentBlockDragging(true);
@ -108,11 +111,10 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
}, },
}) })
); );
}, [canDrag, issueId, groupId, setIsCurrentBlockDragging]); }, [isDraggingAllowed, issueId, groupId, setIsCurrentBlockDragging]);
if (!issue) return null; if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id ?? undefined);
const projectIdentifier = getProjectIdentifierById(issue.project_id); const projectIdentifier = getProjectIdentifierById(issue.project_id);
const isIssueSelected = selectionHelpers.getIsEntitySelected(issue.id); const isIssueSelected = selectionHelpers.getIsEntitySelected(issue.id);
const isIssueActive = selectionHelpers.getIsEntityActive(issue.id); const isIssueActive = selectionHelpers.getIsEntityActive(issue.id);
@ -161,11 +163,13 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
} }
)} )}
onDragStart={() => { onDragStart={() => {
if (!canDrag) { if (!isDraggingAllowed) {
setToast({ setToast({
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING,
title: "Cannot move issue", title: "Cannot move issue",
message: "Drag and drop is disabled for the current grouping", message: !canEditIssueProperties
? "You are not allowed to move this issue"
: "Drag and drop is disabled for the current grouping",
}); });
} }
}} }}

View file

@ -4,7 +4,6 @@ import { MutableRefObject, useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { cn } from "@plane/editor"; import { cn } from "@plane/editor";
// plane packages // plane packages
import { import {
@ -98,7 +97,6 @@ export const ListGroup = observer((props: Props) => {
const isExpanded = !collapsedGroups?.group_by.includes(group.id); const isExpanded = !collapsedGroups?.group_by.includes(group.id);
const groupRef = useRef<HTMLDivElement | null>(null); const groupRef = useRef<HTMLDivElement | null>(null);
const { projectId } = useParams();
const projectState = useProjectState(); const projectState = useProjectState();
const { const {
@ -237,8 +235,7 @@ export const ListGroup = observer((props: Props) => {
isWorkflowDropDisabled, isWorkflowDropDisabled,
]); ]);
const isDragAllowed = const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
!!group_by && DRAG_ALLOWED_GROUPS.includes(group_by) && canEditProperties(projectId?.toString());
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled; const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";

View file

@ -1,20 +1,22 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// hooks
import { ProjectIssueQuickActions } from "@/components/issues";
// components // components
// types import { ProjectIssueQuickActions } from "@/components/issues";
// constants // hooks
import { useUserPermissions } from "@/hooks/store"; import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// components
import { BaseListRoot } from "../base-list-root"; import { BaseListRoot } from "../base-list-root";
export const ListLayout: FC = observer(() => { export const ListLayout: FC = observer(() => {
const { workspaceSlug, projectId } = useParams(); // router
const { workspaceSlug } = useParams();
// hooks
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
if (!workspaceSlug || !projectId) return null; if (!workspaceSlug) return null;
const canEditPropertiesBasedOnProject = (projectId: string) => const canEditPropertiesBasedOnProject = (projectId: string) =>
allowPermissions( allowPermissions(

View file

@ -53,7 +53,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
const stateDetails = getStateById(issue.state_id); const stateDetails = getStateById(issue.state_id);
// auth // auth
const isEditingAllowed = const isEditingAllowed =
allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
issue.project_id ?? undefined
) && !readOnly;
const isArchivingAllowed = handleArchive && isEditingAllowed; const isArchivingAllowed = handleArchive && isEditingAllowed;
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isDeletingAllowed = isEditingAllowed; const isDeletingAllowed = isEditingAllowed;

View file

@ -24,7 +24,10 @@ export type SpreadsheetStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
| EIssuesStoreType.MODULE | EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW; | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
interface IBaseSpreadsheetRoot { interface IBaseSpreadsheetRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean; canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
@ -99,11 +102,11 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)}
portalElement={portalElement} portalElement={portalElement}
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={!canEditProperties(issue.project_id ?? undefined) || isCompletedCycle}
placements={placement} placements={placement}
/> />
), ),
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isCompletedCycle, canEditProperties, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
if (!Array.isArray(issueIds)) return null; if (!Array.isArray(issueIds)) return null;

View file

@ -1,9 +1,33 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// mobx store import { useParams } from "next/navigation";
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; // components
import { ProjectIssueQuickActions } from "@/components/issues";
// hooks
import { useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// local components
import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
export const ProjectSpreadsheetLayout: React.FC = observer(() => ( export const ProjectSpreadsheetLayout: React.FC = observer(() => {
<BaseSpreadsheetRoot QuickActions={ProjectIssueQuickActions} /> // router
)); const { workspaceSlug } = useParams();
// hooks
const { allowPermissions } = useUserPermissions();
// derived values
const canEditPropertiesBasedOnProject = (projectId: string) =>
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
projectId
);
return (
<BaseSpreadsheetRoot
QuickActions={ProjectIssueQuickActions}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
/>
);
});

View file

@ -1,6 +1,8 @@
"use-client"; "use-client";
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// types
import { TNameDescriptionLoader } from "@plane/types";
// components // components
import { IssueParentDetail, TIssueOperations } from "@/components/issues"; import { IssueParentDetail, TIssueOperations } from "@/components/issues";
// helpers // helpers
@ -25,8 +27,8 @@ interface IPeekOverviewIssueDetails {
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
disabled: boolean; disabled: boolean;
isArchived: boolean; isArchived: boolean;
isSubmitting: "submitting" | "submitted" | "saved"; isSubmitting: TNameDescriptionLoader;
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; setIsSubmitting: (value: TNameDescriptionLoader) => void;
} }
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => { export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {

View file

@ -1,5 +1,8 @@
import { FC, useRef, useState } from "react"; import { FC, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// types
import { TNameDescriptionLoader } from "@plane/types";
// components
import { import {
DeleteIssueModal, DeleteIssueModal,
IssuePeekOverviewHeader, IssuePeekOverviewHeader,
@ -49,7 +52,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
} = props; } = props;
// states // states
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek"); const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
// ref // ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null); const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
// store hooks // store hooks

View file

@ -2,6 +2,7 @@
import { FC, useState, useEffect, useCallback } from "react"; import { FC, useState, useEffect, useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TNameDescriptionLoader } from "@plane/types";
// components // components
import { TextArea } from "@plane/ui"; import { TextArea } from "@plane/ui";
// types // types
@ -14,8 +15,8 @@ export type IssueTitleInputProps = {
disabled?: boolean; disabled?: boolean;
value: string | undefined | null; value: string | undefined | null;
workspaceSlug: string; workspaceSlug: string;
isSubmitting: "submitting" | "submitted" | "saved"; isSubmitting: TNameDescriptionLoader;
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; setIsSubmitting: (value: TNameDescriptionLoader) => void;
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
projectId: string; projectId: string;
issueId: string; issueId: string;

View file

@ -61,7 +61,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
<div className="grow"> <div className="grow">
<div className="pb-1 text-base font-medium text-custom-text-100">Property changes</div> <div className="pb-1 text-base font-medium text-custom-text-100">Property changes</div>
<div className="text-sm font-normal text-custom-text-300"> <div className="text-sm font-normal text-custom-text-300">
Notify me when issue's properties like assignees, priority, estimates or anything else changes. Notify me when issue&apos;s properties like assignees, priority, estimates or anything else changes.
</div> </div>
</div> </div>
<div className="shrink-0"> <div className="shrink-0">