Skip to content
13 changes: 13 additions & 0 deletions packages/types/src/inbox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,16 @@ export type TInboxIssuePaginationInfo = TPaginationInfo & {
export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
results: TInboxIssue[];
};

export type TInboxForm = {
anchor: string;
id: string;
is_disabled: boolean;
};

export type TInboxIssueForm = {
name: string;
description: string;
username: string;
email: string;
};
3 changes: 2 additions & 1 deletion packages/ui/src/popovers/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const Popover = (props: TPopover) => {
panelClassName = "",
children,
popoverButtonRef,
buttonRefClassName = "",
} = props;
// states
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
Expand All @@ -38,7 +39,7 @@ export const Popover = (props: TPopover) => {

return (
<HeadlessReactPopover className={cn("relative flex h-full w-full items-center justify-center", popoverClassName)}>
<div ref={setReferenceElement} className="w-full">
<div ref={setReferenceElement} className={cn("w-full", buttonRefClassName)}>
<HeadlessReactPopover.Button
ref={popoverButtonRef as Ref<HTMLButtonElement>}
className={cn(
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/popovers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type TPopoverButtonDefaultOptions = {
// button and button styling
button?: ReactNode;
buttonClassName?: string;
buttonRefClassName?: string;
disabled?: boolean;
};

Expand Down
Binary file added space/public/instance/intake-sent-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added space/public/instance/intake-sent-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// components
import { AppHeader, ContentWrapper } from "@/components/core";
import { ProjectInboxHeader } from "./header";
import { ProjectInboxHeader } from "@/plane-web/components/projects/settings/intake";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Incorrect import path for ProjectInboxHeader component.

The ProjectInboxHeader component is located at web/ce/components/projects/settings/intake/header.tsx, but the import statement in layout.tsx uses the path @/plane-web/components/projects/settings/intake. Unless there is an index.ts file in the intake directory that re-exports ProjectInboxHeader, this import path is incorrect.

Recommendations:

  • Update the import path to @/ce/components/projects/settings/intake/header to accurately reflect the component's location.
  • Alternatively, add an index.ts file in the intake directory that re-exports ProjectInboxHeader, allowing the current import path to remain valid.
🔗 Analysis chain

Approve the change to absolute import, but clarification needed.

The switch from a relative import to an absolute import is a good practice that can improve code maintainability. However, I have a couple of questions:

  1. The new import path includes "settings" (@/plane-web/components/projects/settings/intake). Is this the correct location for a component used in the inbox layout? It seems counterintuitive to have an inbox-related component in a settings folder.

  2. How does this change relate to the PR objective of "enabling issues to be added to the intake system through emails and forms"? The current modification doesn't seem to directly address this feature.

To ensure the component is in the correct location, please run the following command:

This will help us confirm that the import path is correct and the component is appropriately placed in the project structure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify the existence and location of the ProjectInboxHeader component
fd ProjectInboxHeader.tsx --type f

Length of output: 36


Script:

#!/bin/bash
# Perform a case-insensitive search for ProjectInboxHeader.tsx
fd -i "ProjectInboxHeader.tsx" --type f

# Additionally, search for the component definition in .tsx files
rg "export\s+const\s+ProjectInboxHeader" --type=tsx

Length of output: 121


Script:

#!/bin/bash
# Perform a case-insensitive search for ProjectInboxHeader.tsx
fd -i "ProjectInboxHeader.tsx" --type f

# Additionally, search for the component definition in .tsx files using a glob pattern
rg "export\s+const\s+ProjectInboxHeader" -g "*.tsx"

Length of output: 202


export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
return (
Expand Down
1 change: 1 addition & 0 deletions web/ce/components/projects/settings/intake/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./header";
23 changes: 15 additions & 8 deletions web/ce/constants/project/settings/features.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { ReactNode } from "react";
import { FileText, Layers, Timer } from "lucide-react";
import { IProject } from "@plane/types";
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";

export type TProperties = {
property: string;
title: string;
description: string;
icon: ReactNode;
isPro: boolean;
isEnabled: boolean;
renderChildren?: (
currentProjectDetails: IProject,
isAdmin: boolean,
handleSubmit: (featureKey: string, featureProperty: string) => Promise<void>
) => ReactNode;
};
export type TFeatureList = {
[key: string]: {
property: string;
title: string;
description: string;
icon: ReactNode;
isPro: boolean;
isEnabled: boolean;
};
[key: string]: TProperties;
};

export type TProjectFeatures = {
Expand Down
51 changes: 30 additions & 21 deletions web/core/components/project/settings/features-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,40 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
return (
<div
key={featureItemKey}
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4 last:border-b-0"
className="gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4"
>
<div className="flex items-start gap-3">
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
{featureItem.icon}
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
{featureItem.isPro && (
<Tooltip tooltipContent="Pro feature" position="top">
<UpgradeBadge />
</Tooltip>
)}
<div key={featureItemKey} className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
{featureItem.icon}
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
{featureItem.isPro && (
<Tooltip tooltipContent="Pro feature" position="top">
<UpgradeBadge />
</Tooltip>
)}
</div>
<p className="text-sm leading-5 tracking-tight text-custom-text-300">
{featureItem.description}
</p>
</div>
<p className="text-sm leading-5 tracking-tight text-custom-text-300">{featureItem.description}</p>
</div>
</div>

<ToggleSwitch
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
disabled={!featureItem.isEnabled || !isAdmin}
size="sm"
/>
<ToggleSwitch
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
disabled={!featureItem.isEnabled || !isAdmin}
size="sm"
/>
</div>
<div className="pl-14">
{currentProjectDetails?.[featureItem.property as keyof IProject] &&
featureItem.renderChildren &&
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit)}
</div>
</div>
);
})}
Expand Down
28 changes: 27 additions & 1 deletion web/core/services/inbox/inbox-issue.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// types
import type { TInboxIssue, TIssue, TInboxIssueWithPagination } from "@plane/types";
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
// helpers
Expand Down Expand Up @@ -75,4 +75,30 @@ export class InboxIssueService extends APIService {
throw error?.response?.data;
});
}

async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}

async updatePublishForm(workspaceSlug: string, projectId: string, is_disabled: boolean): Promise<TInboxIssue> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`, {
is_disabled,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}

async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxIssue> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
46 changes: 46 additions & 0 deletions web/core/store/inbox/project-inbox.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TInboxIssueSorting,
TInboxIssuePaginationInfo,
TInboxIssueSortingOrderByQueryParam,
TInboxForm,
} from "@plane/types";
// helpers
import { EInboxIssueCurrentTab, EInboxIssueStatus, EPastDurationFilters, getCustomDates } from "@/helpers/inbox.helper";
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface IProjectInboxStore {
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined;
inboxIssues: Record<string, IInboxIssueStore>; // issue_id -> IInboxIssueStore
inboxIssueIds: string[];
intakeForms: Record<string, TInboxForm>;
// computed
inboxFilters: Partial<TInboxIssueFilter>; // computed project inbox filters
inboxSorting: Partial<TInboxIssueSorting>; // computed project inbox sorting
Expand Down Expand Up @@ -68,6 +70,9 @@ export interface IProjectInboxStore {
) => Promise<void>;
fetchInboxPaginationIssues: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchInboxIssueById: (workspaceSlug: string, projectId: string, inboxIssueId: string) => Promise<TInboxIssue>;
fetchIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
toggleIntakeForms: (workspaceSlug: string, projectId: string, isDisabled: boolean) => Promise<void>;
regenerateIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
createInboxIssue: (
workspaceSlug: string,
projectId: string,
Expand All @@ -89,6 +94,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined = undefined;
inboxIssues: Record<string, IInboxIssueStore> = {};
inboxIssueIds: string[] = [];
intakeForms: Record<string, TInboxForm> = {};
// services
inboxIssueService;

Expand All @@ -103,6 +109,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
inboxIssuePaginationInfo: observable,
inboxIssues: observable,
inboxIssueIds: observable,
intakeForms: observable,
// computed
inboxFilters: computed,
inboxSorting: computed,
Expand Down Expand Up @@ -310,6 +317,45 @@ export class ProjectInboxStore implements IProjectInboxStore {
}
};

fetchIntakeForms = async (workspaceSlug: string, projectId: string) => {
try {
const intakeForms = await this.inboxIssueService.retrievePublishForm(workspaceSlug, projectId);
if (intakeForms)
runInAction(() => {
set(this.intakeForms, projectId, intakeForms);
});
} catch {
console.error("Error fetching the publish forms");
}
};

toggleIntakeForms = async (workspaceSlug: string, projectId: string, isDisabled: boolean) => {
try {
runInAction(() => {
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: isDisabled });
});
await this.inboxIssueService.updatePublishForm(workspaceSlug, projectId, isDisabled);
} catch {
console.error("Error fetching the publish forms");
runInAction(() => {
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: !isDisabled });
});
}
};

regenerateIntakeForms = async (workspaceSlug: string, projectId: string) => {
try {
const form = await this.inboxIssueService.regeneratePublishForm(workspaceSlug, projectId);
if (form) {
runInAction(() => {
set(this.intakeForms, projectId, form);
});
}
} catch {
console.error("Error fetching the publish forms");
}
};

/**
* @description fetch intake issues with paginated data
* @param workspaceSlug
Expand Down
1 change: 1 addition & 0 deletions web/ee/components/projects/settings/intake/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "ce/components/projects/settings/intake";