mirror of
https://github.com/makeplane/plane.git
synced 2026-02-25 04:35:21 +01:00
Sync: Community Changes #4287
This commit is contained in:
@@ -76,7 +76,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
|
||||
if intake is None and not project.intake_view:
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
|
||||
return (
|
||||
@@ -246,7 +246,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
|
||||
if intake is None and not project.intake_view:
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,8 +12,14 @@ import { WorkspaceService } from "@/plane-web/services/workspace.service";
|
||||
import { IRouterStore } from "@/store/router.store";
|
||||
import { IUserStore } from "@/store/user";
|
||||
// store
|
||||
<<<<<<<< HEAD:apps/web/core/store/member/workspace/workspace-member.store.ts
|
||||
import type { CoreRootStore } from "../../root.store";
|
||||
import type { IMemberRootStore } from "../index.ts";
|
||||
import { WorkspaceMemberFiltersStore, IWorkspaceMemberFiltersStore } from "./workspace-member-filters.store";
|
||||
========
|
||||
import { CoreRootStore } from "../root.store";
|
||||
import { IMemberRootStore } from ".";
|
||||
>>>>>>>> f812ee2f46c4330e4c066a000ab7cfadf65cfc53:apps/dev-wiki/core/store/member/workspace-member.store.ts
|
||||
|
||||
export interface IWorkspaceMembership {
|
||||
id: string;
|
||||
@@ -25,12 +31,15 @@ export interface IWorkspaceMemberStore {
|
||||
// observables
|
||||
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
|
||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]>;
|
||||
// filters store
|
||||
filtersStore: IWorkspaceMemberFiltersStore;
|
||||
// computed
|
||||
workspaceMemberIds: string[] | null;
|
||||
workspaceMemberInvitationIds: string[] | null;
|
||||
memberMap: Record<string, IWorkspaceMembership> | null;
|
||||
// computed actions
|
||||
getWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||
getFilteredWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
|
||||
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
|
||||
getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null;
|
||||
@@ -57,6 +66,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
|
||||
} = {}; // { workspaceSlug: { userId: userDetails } }
|
||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]> = {}; // { workspaceSlug: [invitations] }
|
||||
// filters store
|
||||
filtersStore: IWorkspaceMemberFiltersStore;
|
||||
// stores
|
||||
routerStore: IRouterStore;
|
||||
userStore: IUserStore;
|
||||
@@ -81,7 +92,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
updateMemberInvitation: action,
|
||||
deleteMemberInvitation: action,
|
||||
});
|
||||
|
||||
// initialize filters store
|
||||
this.filtersStore = new WorkspaceMemberFiltersStore();
|
||||
// root store
|
||||
this.routerStore = _rootStore.router;
|
||||
this.userStore = _rootStore.user;
|
||||
@@ -123,6 +135,25 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the filtered and sorted list of all the user ids of all the members of the workspace
|
||||
* @param workspaceSlug
|
||||
*/
|
||||
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
//filter out bots and inactive members
|
||||
members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||
|
||||
// Use filters store to get filtered member ids
|
||||
const memberIds = this.filtersStore.getFilteredMemberIds(
|
||||
members,
|
||||
this.memberRoot?.memberMap || {},
|
||||
(member) => member.member
|
||||
);
|
||||
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the list of all the user ids that match the search query of all the members of the current workspace
|
||||
* @param searchQuery
|
||||
@@ -130,9 +161,9 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return null;
|
||||
const workspaceMemberIds = this.workspaceMemberIds;
|
||||
if (!workspaceMemberIds) return null;
|
||||
const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => {
|
||||
const filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
|
||||
if (!filteredMemberIds) return null;
|
||||
const searchedWorkspaceMemberIds = filteredMemberIds.filter((userId) => {
|
||||
const memberDetails = this.getWorkspaceMemberDetails(userId);
|
||||
if (!memberDetails) return false;
|
||||
const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${
|
||||
|
||||
@@ -29,7 +29,8 @@ import { IssueTitleInput } from "../title-input";
|
||||
// services init
|
||||
const workItemVersionService = new WorkItemVersionService();
|
||||
|
||||
interface IPeekOverviewIssueDetails {
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
@@ -38,12 +39,11 @@ interface IPeekOverviewIssueDetails {
|
||||
isArchived: boolean;
|
||||
isSubmitting: TNameDescriptionLoader;
|
||||
setIsSubmitting: (value: TNameDescriptionLoader) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer((props) => {
|
||||
const { workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
export const PeekOverviewIssueDetails: FC<Props> = observer((props) => {
|
||||
const { editorRef, workspaceSlug, issueId, issueOperations, disabled, isArchived, isSubmitting, setIsSubmitting } =
|
||||
props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { createPortal } from "react-dom";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { EIssueServiceType, TNameDescriptionLoader } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -55,6 +56,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false);
|
||||
// ref
|
||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// store hooks
|
||||
const {
|
||||
setPeekIssue,
|
||||
@@ -83,8 +85,15 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
usePeekOverviewOutsideClickDetector(
|
||||
issuePeekOverviewRef,
|
||||
() => {
|
||||
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
|
||||
if (!embedIssue) {
|
||||
if (!isAnyModalOpen && !isAnyEpicModalOpen && !isAnyLocalModalOpen && !isAnyCustomerModalOpen) {
|
||||
if (
|
||||
!isAnyModalOpen &&
|
||||
!isAnyEpicModalOpen &&
|
||||
!isAnyLocalModalOpen &&
|
||||
!isAnyDropbarOpen &&
|
||||
!isAnyCustomerModalOpen
|
||||
) {
|
||||
removeRoutePeekId();
|
||||
}
|
||||
}
|
||||
@@ -93,10 +102,10 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
);
|
||||
|
||||
const handleKeyDown = () => {
|
||||
const slashCommandDropdownElement = document.querySelector("#slash-command");
|
||||
const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal");
|
||||
const dropdownElement = document.activeElement?.tagName === "INPUT";
|
||||
if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement && !editorImageFullScreenModalElement) {
|
||||
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
|
||||
if (!isAnyModalOpen && !dropdownElement && !isAnyDropbarOpen && !editorImageFullScreenModalElement) {
|
||||
removeRoutePeekId();
|
||||
const issueElement = document.getElementById(`issue-${issueId}`);
|
||||
if (issueElement) issueElement?.focus();
|
||||
@@ -169,6 +178,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="relative flex flex-col gap-3 px-8 py-5 space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
@@ -209,6 +219,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<div className="relative h-full w-full space-y-6 overflow-auto p-4 py-5">
|
||||
<div className="space-y-3">
|
||||
<PeekOverviewIssueDetails
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { EUserPermissions } from "@plane/constants";
|
||||
import type { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types";
|
||||
// plane-web constants
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { WorkspaceService } from "@/plane-web/services/workspace.service";
|
||||
// types
|
||||
import type { IRouterStore } from "@/store/router.store";
|
||||
import type { IUserStore } from "@/store/user";
|
||||
@@ -15,14 +15,12 @@ import type { IUserStore } from "@/store/user";
|
||||
import type { CoreRootStore } from "../../root.store";
|
||||
import type { IMemberRootStore } from "../index.ts";
|
||||
import { WorkspaceMemberFiltersStore, IWorkspaceMemberFiltersStore } from "./workspace-member-filters.store";
|
||||
|
||||
export interface IWorkspaceMembership {
|
||||
id: string;
|
||||
member: string;
|
||||
role: EUserPermissions;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceMemberStore {
|
||||
// observables
|
||||
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
|
||||
@@ -54,8 +52,8 @@ export interface IWorkspaceMemberStore {
|
||||
data: Partial<IWorkspaceMemberInvitation>
|
||||
) => Promise<void>;
|
||||
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
|
||||
isUserSuspended: (userId: string) => boolean;
|
||||
}
|
||||
|
||||
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
// observables
|
||||
workspaceMemberMap: {
|
||||
@@ -70,7 +68,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
memberRoot: IMemberRootStore;
|
||||
// services
|
||||
workspaceService;
|
||||
|
||||
constructor(_memberRoot: IMemberRootStore, _rootStore: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
@@ -87,6 +84,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
fetchWorkspaceMemberInvitations: action,
|
||||
updateMemberInvitation: action,
|
||||
deleteMemberInvitation: action,
|
||||
isUserSuspended: action,
|
||||
});
|
||||
// initialize filters store
|
||||
this.filtersStore = new WorkspaceMemberFiltersStore();
|
||||
@@ -97,29 +95,24 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
// services
|
||||
this.workspaceService = new WorkspaceService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get the list of all the user ids of all the members of the current workspace
|
||||
*/
|
||||
get workspaceMemberIds() {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return null;
|
||||
|
||||
return this.getWorkspaceMemberIds(workspaceSlug);
|
||||
}
|
||||
|
||||
get memberMap() {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return null;
|
||||
return this.workspaceMemberMap?.[workspaceSlug] ?? {};
|
||||
}
|
||||
|
||||
get workspaceMemberInvitationIds() {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return null;
|
||||
return this.workspaceMemberInvitations?.[workspaceSlug]?.map((inv) => inv.id);
|
||||
}
|
||||
|
||||
getWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
members = sortBy(members, [
|
||||
@@ -127,12 +120,9 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
|
||||
]);
|
||||
//filter out bots
|
||||
const memberIds = members
|
||||
.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot)
|
||||
.map((m) => m.member);
|
||||
const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member);
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the filtered and sorted list of all the user ids of all the members of the workspace
|
||||
* @param workspaceSlug
|
||||
@@ -140,18 +130,15 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
//filter out bots and inactive members
|
||||
members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||
|
||||
members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||
// Use filters store to get filtered member ids
|
||||
const memberIds = this.filtersStore.getFilteredMemberIds(
|
||||
members,
|
||||
this.memberRoot?.memberMap || {},
|
||||
(member) => member.member
|
||||
);
|
||||
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the list of all the user ids that match the search query of all the members of the current workspace
|
||||
* @param searchQuery
|
||||
@@ -159,9 +146,9 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return null;
|
||||
const workspaceMemberIds = this.workspaceMemberIds;
|
||||
if (!workspaceMemberIds) return null;
|
||||
const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => {
|
||||
const filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
|
||||
if (!filteredMemberIds) return null;
|
||||
const searchedWorkspaceMemberIds = filteredMemberIds.filter((userId) => {
|
||||
const memberDetails = this.getWorkspaceMemberDetails(userId);
|
||||
if (!memberDetails) return false;
|
||||
const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${
|
||||
@@ -171,7 +158,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
});
|
||||
return searchedWorkspaceMemberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the list of all the invitation ids that match the search query of all the member invitations of the current workspace
|
||||
* @param searchQuery
|
||||
@@ -189,7 +175,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
});
|
||||
return searchedWorkspaceMemberInvitationIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the details of a workspace member
|
||||
* @param userId
|
||||
@@ -199,7 +184,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
if (!workspaceSlug) return null;
|
||||
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
|
||||
if (!workspaceMember) return null;
|
||||
|
||||
const memberDetails: IWorkspaceMember = {
|
||||
id: workspaceMember.id,
|
||||
role: workspaceMember.role,
|
||||
@@ -208,7 +192,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
};
|
||||
return memberDetails;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the details of a workspace member invitation
|
||||
* @param workspaceSlug
|
||||
@@ -219,11 +202,9 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
if (!workspaceSlug) return null;
|
||||
const invitationsList = this.workspaceMemberInvitations?.[workspaceSlug];
|
||||
if (!invitationsList) return null;
|
||||
|
||||
const invitation = invitationsList.find((inv) => inv.id === invitationId);
|
||||
return invitation ?? null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetch all the members of a workspace
|
||||
* @param workspaceSlug
|
||||
@@ -243,7 +224,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description update the role of a workspace member
|
||||
* @param workspaceSlug
|
||||
@@ -268,7 +248,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description remove a member from workspace
|
||||
* @param workspaceSlug
|
||||
@@ -283,7 +262,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetch all the member invitations of a workspace
|
||||
* @param workspaceSlug
|
||||
@@ -295,7 +273,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description bulk invite members to a workspace
|
||||
* @param workspaceSlug
|
||||
@@ -306,7 +283,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
await this.fetchWorkspaceMemberInvitations(workspaceSlug);
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update the role of a member invitation
|
||||
* @param workspaceSlug
|
||||
@@ -337,7 +313,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description delete a member invitation
|
||||
* @param workspaceSlug
|
||||
@@ -351,4 +326,10 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
);
|
||||
});
|
||||
});
|
||||
isUserSuspended = computedFn((userId: string) => {
|
||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||
if (!workspaceSlug) return false;
|
||||
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
|
||||
return workspaceMember?.is_active === false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, useRef } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
@@ -16,6 +16,7 @@ import { EpicInfoActionItems } from "./info-section/action-items";
|
||||
import { EpicInfoIndicatorItem } from "./info-section/indicator-item";
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
epicId: string;
|
||||
@@ -23,9 +24,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const EpicInfoSection: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, epicId, disabled = false } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const { editorRef, workspaceSlug, projectId, epicId, disabled = false } = props;
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// components
|
||||
import { IssueDetailWidgets } from "@/components/issues/issue-detail-widgets/root";
|
||||
@@ -15,6 +16,7 @@ import { EpicOverviewRoot } from "./overview-section-root";
|
||||
import { EpicProgressSection } from "./progress-section-root";
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
epicId: string;
|
||||
@@ -22,13 +24,19 @@ type Props = {
|
||||
};
|
||||
|
||||
export const EpicMainContentRoot: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, epicId, disabled = false } = props;
|
||||
const { editorRef, workspaceSlug, projectId, epicId, disabled = false } = props;
|
||||
// store hooks
|
||||
const { epicDetailSidebarCollapsed } = useAppTheme();
|
||||
|
||||
return (
|
||||
<MainWrapper isSidebarOpen={!epicDetailSidebarCollapsed}>
|
||||
<EpicInfoSection workspaceSlug={workspaceSlug} projectId={projectId} epicId={epicId} disabled={disabled} />
|
||||
<EpicInfoSection
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
epicId={epicId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<EpicProgressSection epicId={epicId} />
|
||||
<div className="py-2">
|
||||
<IssueDetailWidgets
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { EIssueServiceType, EIssuesStoreType, EUserProjectRoles } from "@plane/types";
|
||||
// components
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
@@ -20,13 +21,14 @@ import { EpicMainContentRoot } from "./main/root";
|
||||
import { EpicDetailsSidebar } from "./sidebar/root";
|
||||
|
||||
export type TIssueDetailRoot = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
epicId: string;
|
||||
};
|
||||
|
||||
export const EpicDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, epicId } = props;
|
||||
const { editorRef, workspaceSlug, projectId, epicId } = props;
|
||||
// hooks
|
||||
const { fetchEpicAnalytics } = useEpicAnalytics();
|
||||
const {
|
||||
@@ -62,6 +64,7 @@ export const EpicDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
emptyStateComponent={<EpicEmptyState workspaceSlug={workspaceSlug} projectId={projectId} />}
|
||||
>
|
||||
<EpicMainContentRoot
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
epicId={epicId}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { FC, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { createPortal } from "react-dom";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import type { TIssueOperations } from "@/components/issues/issue-detail";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
@@ -51,8 +53,9 @@ export const EpicView: FC<IEpicView> = observer((props) => {
|
||||
const [deleteEpicModal, setDeleteEpicModal] = useState(false);
|
||||
const [editEpicModal, setEditEpicModal] = useState(false);
|
||||
const [duplicateEpicModal, setDuplicateEpicModal] = useState(false);
|
||||
// ref
|
||||
// refs
|
||||
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// store hooks
|
||||
const {
|
||||
setPeekIssue,
|
||||
@@ -78,6 +81,7 @@ export const EpicView: FC<IEpicView> = observer((props) => {
|
||||
usePeekOverviewOutsideClickDetector(
|
||||
issuePeekOverviewRef,
|
||||
() => {
|
||||
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
|
||||
if (!embedIssue) {
|
||||
if (
|
||||
!isAnyModalOpen &&
|
||||
@@ -88,7 +92,8 @@ export const EpicView: FC<IEpicView> = observer((props) => {
|
||||
!isAnyCustomerModalOpen &&
|
||||
!isInitiativeModalOpen &&
|
||||
!duplicateEpicModal &&
|
||||
!isPeekOpen
|
||||
!isPeekOpen &&
|
||||
!isAnyDropbarOpen
|
||||
) {
|
||||
removeRoutePeekId();
|
||||
}
|
||||
@@ -98,10 +103,10 @@ export const EpicView: FC<IEpicView> = observer((props) => {
|
||||
);
|
||||
|
||||
const handleKeyDown = () => {
|
||||
const slashCommandDropdownElement = document.querySelector("#slash-command");
|
||||
const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal");
|
||||
const dropdownElement = document.activeElement?.tagName === "INPUT";
|
||||
if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement && !editorImageFullScreenModalElement) {
|
||||
const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen();
|
||||
if (!isAnyModalOpen && !dropdownElement && !isAnyDropbarOpen && !editorImageFullScreenModalElement) {
|
||||
removeRoutePeekId();
|
||||
const issueElement = document.getElementById(`issue-${issueId}`);
|
||||
if (issueElement) issueElement?.focus();
|
||||
@@ -175,6 +180,7 @@ export const EpicView: FC<IEpicView> = observer((props) => {
|
||||
/>
|
||||
{/* content */}
|
||||
<EpicDetailRoot
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
epicId={issueId.toString()}
|
||||
|
||||
@@ -162,7 +162,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
{!isSelecting && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg max-w-[500px] overflow-x-scroll horizontal-scrollbar scrollbar-xs"
|
||||
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg overflow-x-scroll horizontal-scrollbar scrollbar-xs"
|
||||
>
|
||||
<div className="px-2">
|
||||
<BubbleMenuNodeSelector
|
||||
|
||||
@@ -73,6 +73,9 @@ const Command = Extension.create<SlashCommandOptions>({
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
// Track active dropdown
|
||||
props.editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.SLASH_COMMANDS);
|
||||
|
||||
const MenuComponent = SlashCommandsMenu as unknown as FC<
|
||||
SlashCommandsMenuProps & { ref: React.Ref<CommandListInstance> }
|
||||
>;
|
||||
@@ -116,7 +119,9 @@ const Command = Extension.create<SlashCommandOptions>({
|
||||
return component?.ref?.onKeyDown(props) ?? false;
|
||||
},
|
||||
|
||||
onExit: () => {
|
||||
onExit: ({ editor }) => {
|
||||
// Remove from active dropdowns
|
||||
editor?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.SLASH_COMMANDS);
|
||||
component?.destroy();
|
||||
component = null;
|
||||
},
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Ellipsis } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import {
|
||||
findTable,
|
||||
@@ -59,7 +61,16 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
|
||||
}),
|
||||
],
|
||||
open: isDropdownOpen,
|
||||
onOpenChange: setIsDropdownOpen,
|
||||
onOpenChange: (open) => {
|
||||
setIsDropdownOpen(open);
|
||||
if (open) {
|
||||
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
const click = useClick(context);
|
||||
@@ -185,7 +196,6 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
|
||||
}}
|
||||
lockScroll
|
||||
/>
|
||||
|
||||
<div
|
||||
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
|
||||
ref={refs.setFloating}
|
||||
@@ -195,7 +205,7 @@ export const ColumnDragHandle: React.FC<ColumnDragHandleProps> = (props) => {
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<ColumnOptionsDropdown editor={editor} onClose={() => setIsDropdownOpen(false)} />
|
||||
<ColumnOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Ellipsis } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import {
|
||||
findTable,
|
||||
@@ -59,7 +61,16 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
|
||||
}),
|
||||
],
|
||||
open: isDropdownOpen,
|
||||
onOpenChange: setIsDropdownOpen,
|
||||
onOpenChange: (open) => {
|
||||
setIsDropdownOpen(open);
|
||||
if (open) {
|
||||
editor.commands.addActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
editor.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.TABLE);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
const click = useClick(context);
|
||||
@@ -184,7 +195,6 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
|
||||
}}
|
||||
lockScroll
|
||||
/>
|
||||
|
||||
<div
|
||||
className="max-h-[90vh] w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
|
||||
ref={refs.setFloating}
|
||||
@@ -194,7 +204,7 @@ export const RowDragHandle: React.FC<RowDragHandleProps> = (props) => {
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<RowOptionsDropdown editor={editor} onClose={() => setIsDropdownOpen(false)} />
|
||||
<RowOptionsDropdown editor={editor} onClose={() => context.onOpenChange(false)} />
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
|
||||
@@ -11,11 +11,16 @@ import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
|
||||
import { codemark } from "./code-mark";
|
||||
|
||||
type TActiveDropbarExtensions = CORE_EXTENSIONS.MENTION | CORE_EXTENSIONS.EMOJI | TAdditionalActiveDropbarExtensions;
|
||||
type TActiveDropbarExtensions =
|
||||
| CORE_EXTENSIONS.MENTION
|
||||
| CORE_EXTENSIONS.EMOJI
|
||||
| CORE_EXTENSIONS.SLASH_COMMANDS
|
||||
| CORE_EXTENSIONS.TABLE
|
||||
| TAdditionalActiveDropbarExtensions;
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands {
|
||||
utility: {
|
||||
[CORE_EXTENSIONS.UTILITY]: {
|
||||
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
|
||||
updateAssetsList: (
|
||||
args:
|
||||
@@ -26,6 +31,8 @@ declare module "@tiptap/core" {
|
||||
idToRemove: string;
|
||||
}
|
||||
) => () => void;
|
||||
addActiveDropbarExtension: (extension: TActiveDropbarExtensions) => () => void;
|
||||
removeActiveDropbarExtension: (extension: TActiveDropbarExtensions) => () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -102,6 +109,18 @@ export const UtilityExtension = (props: Props) => {
|
||||
}
|
||||
this.storage.assetsList = Array.from(uniqueAssets);
|
||||
},
|
||||
addActiveDropbarExtension: (extension) => () => {
|
||||
const index = this.storage.activeDropbarExtensions.indexOf(extension);
|
||||
if (index === -1) {
|
||||
this.storage.activeDropbarExtensions.push(extension);
|
||||
}
|
||||
},
|
||||
removeActiveDropbarExtension: (extension) => () => {
|
||||
const index = this.storage.activeDropbarExtensions.indexOf(extension);
|
||||
if (index !== -1) {
|
||||
this.storage.activeDropbarExtensions.splice(index, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -87,6 +87,11 @@ export const getEditorRefHelpers = (args: TArgs): EditorRefApi => {
|
||||
const markdownOutput = editor?.storage?.markdown?.getMarkdown?.();
|
||||
return markdownOutput;
|
||||
},
|
||||
isAnyDropbarOpen: () => {
|
||||
if (!editor) return false;
|
||||
const utilityStorage = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY);
|
||||
return utilityStorage.activeDropbarExtensions.length > 0;
|
||||
},
|
||||
scrollSummary: (marking) => {
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
|
||||
@@ -133,6 +133,7 @@ export type CoreEditorRefApi = {
|
||||
getMarkDown: () => string;
|
||||
getSelectedText: () => string | null;
|
||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||
isAnyDropbarOpen: () => boolean;
|
||||
isEditorReadyToDiscard: () => boolean;
|
||||
isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
|
||||
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
|
||||
|
||||
@@ -225,7 +225,7 @@ ul[data-type="taskList"] li[data-checked="true"] {
|
||||
/* Overwrite tippy-box original max-width */
|
||||
|
||||
.tippy-box {
|
||||
max-width: 600px !important;
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
|
||||
Reference in New Issue
Block a user