Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import { ReactNode } from "react";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import { ProjectsListHeader } from "@/plane-web/components/projects/header";
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
export default function ProjectListLayout({ children }: { children: ReactNode }) {
return (
<>
<AppHeader header={<ProjectsListHeader />} mobileHeader={<ProjectsListMobileHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ProjectPageRoot from "@/plane-web/components/projects/page";

const ProjectsPage = () => <ProjectPageRoot />;
export default ProjectsPage;
5 changes: 2 additions & 3 deletions web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { ReactNode } from "react";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import { ProjectsListHeader } from "./header";
import { ProjectsListMobileHeader } from "./mobile-header";

import { ProjectsListHeader } from "@/plane-web/components/projects/header";
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
export default function ProjectListLayout({ children }: { children: ReactNode }) {
return (
<>
Expand Down
84 changes: 2 additions & 82 deletions web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,4 @@
"use client";

import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store";

const ProjectsPage = observer(() => {
// store
const { workspaceSlug } = useParams();
const { currentWorkspace } = useWorkspace();
const { totalProjectIds, filteredProjectIds } = useProject();
const {
currentWorkspaceFilters,
currentWorkspaceAppliedDisplayFilters,
clearAllFilters,
clearAllAppliedDisplayFilters,
updateFilters,
updateDisplayFilters,
} = useProjectFilter();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined;

const handleRemoveFilter = useCallback(
(key: keyof TProjectFilters, value: string | null) => {
if (!workspaceSlug) return;
let newValues = currentWorkspaceFilters?.[key] ?? [];

if (!value) newValues = [];
else newValues = newValues.filter((val) => val !== value);

updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[currentWorkspaceFilters, updateFilters, workspaceSlug]
);

const handleRemoveDisplayFilter = useCallback(
(key: TProjectAppliedDisplayFilterKeys) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), { [key]: false });
},
[updateDisplayFilters, workspaceSlug]
);

const handleClearAllFilters = useCallback(() => {
if (!workspaceSlug) return;
clearAllFilters(workspaceSlug.toString());
clearAllAppliedDisplayFilters(workspaceSlug.toString());
}, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]);

return (
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col">
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 ||
currentWorkspaceAppliedDisplayFilters?.length !== 0) && (
<div className="border-b border-custom-border-200 px-5 py-3">
<ProjectAppliedFiltersList
appliedFilters={currentWorkspaceFilters ?? {}}
appliedDisplayFilters={currentWorkspaceAppliedDisplayFilters ?? []}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
handleRemoveDisplayFilter={handleRemoveDisplayFilter}
filteredProjects={filteredProjectIds?.length ?? 0}
totalProjects={totalProjectIds?.length ?? 0}
alwaysAllowEditing
/>
</div>
)}
<ProjectCardList />
</div>
</>
);
});
import ProjectPageRoot from "@/plane-web/components/projects/page";

const ProjectsPage = () => <ProjectPageRoot />;
export default ProjectsPage;
80 changes: 80 additions & 0 deletions web/ce/components/projects/create/attributes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Controller, useFormContext } from "react-hook-form";
import { IProject } from "@plane/types";
import { CustomSelect } from "@plane/ui";
import { MemberDropdown } from "@/components/dropdowns";
import { NETWORK_CHOICES } from "@/constants/project";

const ProjectAttributes = () => {
const { control } = useFormContext<IProject>();
return (
<div className="flex flex-wrap items-center gap-2">
<Controller
name="network"
control={control}
render={({ field: { onChange, value } }) => {
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value);

return (
<div className="flex-shrink-0 h-7" tabIndex={4}>
<CustomSelect
value={value}
onChange={onChange}
label={
<div className="flex items-center gap-1 h-full">
{currentNetwork ? (
<>
<currentNetwork.icon className="h-3 w-3" />
{currentNetwork.label}
</>
) : (
<span className="text-custom-text-400">Select network</span>
)}
</div>
}
placement="bottom-start"
className="h-full"
buttonClassName="h-full"
noChevron
tabIndex={4}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
<div className="flex items-start gap-2">
<network.icon className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{network.label}</p>
<p className="text-xs text-custom-text-400">{network.description}</p>
</div>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
);
}}
/>
<Controller
name="project_lead"
control={control}
render={({ field: { value, onChange } }) => {
if (value === undefined || value === null || typeof value === "string")
return (
<div className="flex-shrink-0 h-7" tabIndex={5}>
<MemberDropdown
value={value}
onChange={(lead) => onChange(lead === value ? null : lead)}
placeholder="Lead"
multiple={false}
buttonVariant="border-with-text"
tabIndex={5}
/>
</div>
);
else return <></>;
}}
/>
</div>
);
};

export default ProjectAttributes;
139 changes: 139 additions & 0 deletions web/ce/components/projects/create/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { useState, FC } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
// constants
import ProjectCommonAttributes from "@/components/project/create/common-attributes";
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
import { PROJECT_CREATED } from "@/constants/event-tracker";
import { PROJECT_UNSPLASH_COVERS } from "@/constants/project";
// helpers
import { getRandomEmoji } from "@/helpers/emoji.helper";
// hooks
import { useEventTracker, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { TProject } from "@/plane-web/types/projects";
import ProjectAttributes from "./attributes";

type Props = {
setToFavorite?: boolean;
workspaceSlug: string;
onClose: () => void;
handleNextStep: (projectId: string) => void;
data?: Partial<TProject>;
};

const defaultValues: Partial<TProject> = {
cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)],
description: "",
logo_props: {
in_use: "emoji",
emoji: {
value: getRandomEmoji(),
},
},
identifier: "",
name: "",
network: 2,
project_lead: null,
};

export const CreateProjectForm: FC<Props> = observer((props) => {
const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props;
// store
const { captureProjectEvent } = useEventTracker();
const { addProjectToFavorites, createProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider initializing state with a function.

To avoid potential performance issues, consider initializing the state with a function.

- const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
+ const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(() => true);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(() => true);

// form info
const methods = useForm<TProject>({
defaultValues,
reValidateMode: "onChange",
});
const { handleSubmit, reset, setValue } = methods;
const { isMobile } = usePlatformOS();
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;

addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
});
});
};
Comment on lines +59 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

Improve error handling in handleAddToFavorites.

Consider adding more specific error handling and logging to help with debugging.

- addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
-   setToast({
-     type: TOAST_TYPE.ERROR,
-     title: "Error!",
-     message: "Couldn't remove the project from favorites. Please try again.",
-   });
- });
+ addProjectToFavorites(workspaceSlug.toString(), projectId).catch((error) => {
+   console.error("Failed to add project to favorites:", error);
+   setToast({
+     type: TOAST_TYPE.ERROR,
+     title: "Error!",
+     message: "Couldn't remove the project from favorites. Please try again.",
+   });
+ });
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;
addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
});
});
};
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;
addProjectToFavorites(workspaceSlug.toString(), projectId).catch((error) => {
console.error("Failed to add project to favorites:", error);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
});
});
};


const onSubmit = async (formData: Partial<TProject>) => {
// Upper case identifier
formData.identifier = formData.identifier?.toUpperCase();

return createProject(workspaceSlug.toString(), formData)
.then((res) => {
const newPayload = {
...res,
state: "SUCCESS",
};
captureProjectEvent({
eventName: PROJECT_CREATED,
payload: newPayload,
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Project created successfully.",
});
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleNextStep(res.id);
})
.catch((err) => {
Object.keys(err.data).map((key) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err.data[key],
});
captureProjectEvent({
eventName: PROJECT_CREATED,
payload: {
...formData,
state: "FAILED",
},
});
});
});
};

const handleClose = () => {
onClose();
setIsChangeInIdentifierRequired(true);
setTimeout(() => {
reset();
}, 300);
};

return (
<FormProvider {...methods}>
<ProjectCreateHeader handleClose={handleClose} />

<form onSubmit={handleSubmit(onSubmit)} className="px-3">
<div className="mt-9 space-y-6 pb-5">
<ProjectCommonAttributes
setValue={setValue}
isMobile={isMobile}
isChangeInIdentifierRequired={isChangeInIdentifierRequired}
setIsChangeInIdentifierRequired={setIsChangeInIdentifierRequired}
/>
<ProjectAttributes />
</div>
<ProjectCreateButtons handleClose={handleClose} />
</form>
</FormProvider>
);
});
5 changes: 5 additions & 0 deletions web/ce/components/projects/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use client";

import { ProjectsBaseHeader } from "@/components/project/header";

export const ProjectsListHeader = () => <ProjectsBaseHeader />;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
Expand All @@ -23,7 +24,6 @@ export const ProjectsListMobileHeader = observer(() => {
updateFilters,
} = useProjectFilter();


const {
workspace: { workspaceMemberIds },
} = useMember();
Expand Down
5 changes: 5 additions & 0 deletions web/ce/components/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Root from "@/components/project/root";

const ProjectPageRoot = () => <Root />;

export default ProjectPageRoot;
1 change: 1 addition & 0 deletions web/ce/types/projects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./projects";
3 changes: 3 additions & 0 deletions web/ce/types/projects/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { IProject } from "@plane/types";

export type TProject = IProject;
Loading