chore: seperated project components for CE (#5324)
* chore: seperated project components for CE * chore: splitted the code for project creation form * fix: code structure optimization * fix: project page root moved * fix: synced with preview * fix: component splitting and refactoring * fix: build error
This commit is contained in:
parent
3ffaa4f2ca
commit
91693b2269
34 changed files with 799 additions and 596 deletions
80
web/ce/components/projects/create/attributes.tsx
Normal file
80
web/ce/components/projects/create/attributes.tsx
Normal file
|
|
@ -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
web/ce/components/projects/create/root.tsx
Normal file
139
web/ce/components/projects/create/root.tsx
Normal file
|
|
@ -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);
|
||||
// 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.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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
web/ce/components/projects/header.tsx
Normal file
5
web/ce/components/projects/header.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { ProjectsBaseHeader } from "@/components/project/header";
|
||||
|
||||
export const ProjectsListHeader = () => <ProjectsBaseHeader />;
|
||||
91
web/ce/components/projects/mobile-header.tsx
Normal file
91
web/ce/components/projects/mobile-header.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"use client";
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChevronDown, ListFilter } from "lucide-react";
|
||||
// types
|
||||
import { TProjectFilters } from "@plane/types";
|
||||
// hooks
|
||||
import { FiltersDropdown } from "@/components/issues/issue-layouts";
|
||||
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project/dropdowns";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useMember, useProjectFilter } from "@/hooks/store";
|
||||
|
||||
export const ProjectsListMobileHeader = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const {
|
||||
currentWorkspaceDisplayFilters: displayFilters,
|
||||
currentWorkspaceFilters: filters,
|
||||
updateDisplayFilters,
|
||||
updateFilters,
|
||||
} = useProjectFilter();
|
||||
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
} = useMember();
|
||||
|
||||
const handleFilters = useCallback(
|
||||
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||
if (!workspaceSlug) return;
|
||||
const newValues = filters?.[key] ?? [];
|
||||
if (Array.isArray(value))
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
updateFilters(workspaceSlug.toString(), { [key]: newValues });
|
||||
},
|
||||
[filters, updateFilters, workspaceSlug]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
|
||||
|
||||
return (
|
||||
<div className="flex py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100 w-full">
|
||||
<ProjectOrderByDropdown
|
||||
value={displayFilters?.order_by}
|
||||
onChange={(val) => {
|
||||
if (!workspaceSlug || val === displayFilters?.order_by) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), {
|
||||
order_by: val,
|
||||
});
|
||||
}}
|
||||
isMobile
|
||||
/>
|
||||
<div className="border-l border-custom-border-200 flex justify-around w-full">
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<div className="flex text-sm items-center gap-2 neutral-primary text-custom-text-200">
|
||||
<ListFilter className="h-3 w-3" />
|
||||
Filters
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
isFiltersApplied={isFiltersApplied}
|
||||
>
|
||||
<ProjectFiltersSelection
|
||||
displayFilters={displayFilters ?? {}}
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={handleFilters}
|
||||
handleDisplayFiltersUpdate={(val) => {
|
||||
if (!workspaceSlug) return;
|
||||
updateDisplayFilters(workspaceSlug.toString(), val);
|
||||
}}
|
||||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
5
web/ce/components/projects/page.tsx
Normal file
5
web/ce/components/projects/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Root from "@/components/project/root";
|
||||
|
||||
const ProjectPageRoot = () => <Root />;
|
||||
|
||||
export default ProjectPageRoot;
|
||||
1
web/ce/types/projects/index.ts
Normal file
1
web/ce/types/projects/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./projects";
|
||||
3
web/ce/types/projects/projects.ts
Normal file
3
web/ce/types/projects/projects.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { IProject } from "@plane/types";
|
||||
|
||||
export type TProject = IProject;
|
||||
Loading…
Add table
Add a link
Reference in a new issue