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
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import ProjectPageRoot from "@/plane-web/components/projects/page";
|
||||||
|
|
||||||
|
const ProjectsPage = () => <ProjectPageRoot />;
|
||||||
|
export default ProjectsPage;
|
||||||
|
|
@ -4,9 +4,8 @@ import { ReactNode } from "react";
|
||||||
// components
|
// components
|
||||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||||
// local components
|
// local components
|
||||||
import { ProjectsListHeader } from "./header";
|
import { ProjectsListHeader } from "@/plane-web/components/projects/header";
|
||||||
import { ProjectsListMobileHeader } from "./mobile-header";
|
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
|
||||||
|
|
||||||
export default function ProjectListLayout({ children }: { children: ReactNode }) {
|
export default function ProjectListLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,4 @@
|
||||||
"use client";
|
import ProjectPageRoot from "@/plane-web/components/projects/page";
|
||||||
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const ProjectsPage = () => <ProjectPageRoot />;
|
||||||
export default ProjectsPage;
|
export default ProjectsPage;
|
||||||
|
|
|
||||||
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 />;
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
@ -23,7 +24,6 @@ export const ProjectsListMobileHeader = observer(() => {
|
||||||
updateFilters,
|
updateFilters,
|
||||||
} = useProjectFilter();
|
} = useProjectFilter();
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
workspace: { workspaceMemberIds },
|
workspace: { workspaceMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
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;
|
||||||
|
|
@ -14,10 +14,10 @@ export type GanttChartBlocksProps = {
|
||||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any) => React.ReactNode;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockMove: boolean;
|
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
selectionHelpers: TSelectionHelper;
|
selectionHelpers: TSelectionHelper;
|
||||||
|
|
@ -55,10 +55,14 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
blockToRender={blockToRender}
|
blockToRender={blockToRender}
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
enableBlockLeftResize={
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
typeof enableBlockLeftResize === "function" ? enableBlockLeftResize(blockId) : enableBlockLeftResize
|
||||||
enableBlockMove={enableBlockMove}
|
}
|
||||||
enableAddBlock={enableAddBlock}
|
enableBlockRightResize={
|
||||||
|
typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize
|
||||||
|
}
|
||||||
|
enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove}
|
||||||
|
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
|
||||||
ganttContainerRef={ganttContainerRef}
|
ganttContainerRef={ganttContainerRef}
|
||||||
selectionHelpers={selectionHelpers}
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,12 @@ type Props = {
|
||||||
handleToday: () => void;
|
handleToday: () => void;
|
||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
toggleFullScreenMode: () => void;
|
toggleFullScreenMode: () => void;
|
||||||
|
showToday: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||||
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props;
|
const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } =
|
||||||
|
props;
|
||||||
// chart hook
|
// chart hook
|
||||||
const { currentView } = useGanttChart();
|
const { currentView } = useGanttChart();
|
||||||
|
|
||||||
|
|
@ -46,9 +48,15 @@ export const GanttChartHeader: React.FC<Props> = observer((props) => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80" onClick={handleToday}>
|
{showToday && (
|
||||||
Today
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
|
||||||
|
onClick={handleToday}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ import {
|
||||||
WeekChartView,
|
WeekChartView,
|
||||||
YearChartView,
|
YearChartView,
|
||||||
} from "@/components/gantt-chart";
|
} from "@/components/gantt-chart";
|
||||||
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||||
// helpers
|
|
||||||
// constants
|
// constants
|
||||||
import { GANTT_SELECT_GROUP } from "../constants";
|
import { GANTT_SELECT_GROUP } from "../constants";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -38,12 +38,12 @@ type Props = {
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any) => React.ReactNode;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
bottomSpacing: boolean;
|
bottomSpacing: boolean;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockMove: boolean;
|
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||||
enableReorder: boolean;
|
enableReorder: boolean | ((blockId: string) => boolean);
|
||||||
enableSelection: boolean;
|
enableSelection: boolean | ((blockId: string) => boolean);
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||||
itemsContainerWidth: number;
|
itemsContainerWidth: number;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
|
|
@ -145,38 +145,38 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
<GanttChartSidebar
|
<GanttChartSidebar
|
||||||
blockIds={blockIds}
|
blockIds={blockIds}
|
||||||
getBlockById={getBlockById}
|
getBlockById={getBlockById}
|
||||||
loadMoreBlocks={loadMoreBlocks}
|
loadMoreBlocks={loadMoreBlocks}
|
||||||
canLoadMoreBlocks={canLoadMoreBlocks}
|
canLoadMoreBlocks={canLoadMoreBlocks}
|
||||||
ganttContainerRef={ganttContainerRef}
|
ganttContainerRef={ganttContainerRef}
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
enableReorder={enableReorder}
|
enableReorder={enableReorder}
|
||||||
enableSelection={enableSelection}
|
enableSelection={enableSelection}
|
||||||
sidebarToRender={sidebarToRender}
|
sidebarToRender={sidebarToRender}
|
||||||
title={title}
|
title={title}
|
||||||
quickAdd={quickAdd}
|
quickAdd={quickAdd}
|
||||||
selectionHelpers={helpers}
|
selectionHelpers={helpers}
|
||||||
/>
|
/>
|
||||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||||
<ActiveChartView />
|
<ActiveChartView />
|
||||||
{currentViewData && (
|
{currentViewData && (
|
||||||
<GanttChartBlocksList
|
<GanttChartBlocksList
|
||||||
itemsContainerWidth={itemsContainerWidth}
|
itemsContainerWidth={itemsContainerWidth}
|
||||||
blockIds={blockIds}
|
blockIds={blockIds}
|
||||||
getBlockById={getBlockById}
|
getBlockById={getBlockById}
|
||||||
blockToRender={blockToRender}
|
blockToRender={blockToRender}
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
enableBlockMove={enableBlockMove}
|
enableBlockMove={enableBlockMove}
|
||||||
enableAddBlock={enableAddBlock}
|
enableAddBlock={enableAddBlock}
|
||||||
ganttContainerRef={ganttContainerRef}
|
ganttContainerRef={ganttContainerRef}
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
selectionHelpers={helpers}
|
selectionHelpers={helpers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,19 @@ type ChartViewRootProps = {
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any) => React.ReactNode;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockMove: boolean;
|
enableBlockMove: boolean | ((blockId: string) => boolean);
|
||||||
enableReorder: boolean;
|
enableReorder: boolean | ((blockId: string) => boolean);
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||||
enableSelection: boolean;
|
enableSelection: boolean | ((blockId: string) => boolean);
|
||||||
bottomSpacing: boolean;
|
bottomSpacing: boolean;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||||
loadMoreBlocks?: () => void;
|
loadMoreBlocks?: () => void;
|
||||||
canLoadMoreBlocks?: boolean;
|
canLoadMoreBlocks?: boolean;
|
||||||
quickAdd?: React.JSX.Element | undefined;
|
quickAdd?: React.JSX.Element | undefined;
|
||||||
|
showToday: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||||
|
|
@ -58,6 +59,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||||
bottomSpacing,
|
bottomSpacing,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
quickAdd,
|
quickAdd,
|
||||||
|
showToday,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
|
||||||
|
|
@ -161,6 +163,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||||
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
|
||||||
handleToday={handleToday}
|
handleToday={handleToday}
|
||||||
loaderTitle={loaderTitle}
|
loaderTitle={loaderTitle}
|
||||||
|
showToday={showToday}
|
||||||
/>
|
/>
|
||||||
<GanttChartMainContent
|
<GanttChartMainContent
|
||||||
blockIds={blockIds}
|
blockIds={blockIds}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@ type GanttChartRootProps = {
|
||||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||||
canLoadMoreBlocks?: boolean;
|
canLoadMoreBlocks?: boolean;
|
||||||
loadMoreBlocks?: () => void;
|
loadMoreBlocks?: () => void;
|
||||||
enableBlockLeftResize?: boolean;
|
enableBlockLeftResize?: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockRightResize?: boolean;
|
enableBlockRightResize?: boolean | ((blockId: string) => boolean);
|
||||||
enableBlockMove?: boolean;
|
enableBlockMove?: boolean | ((blockId: string) => boolean);
|
||||||
enableReorder?: boolean;
|
enableReorder?: boolean | ((blockId: string) => boolean);
|
||||||
enableAddBlock?: boolean;
|
enableAddBlock?: boolean | ((blockId: string) => boolean);
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean | ((blockId: string) => boolean);
|
||||||
bottomSpacing?: boolean;
|
bottomSpacing?: boolean;
|
||||||
showAllBlocks?: boolean;
|
showAllBlocks?: boolean;
|
||||||
|
showToday?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||||
|
|
@ -46,6 +47,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||||
enableSelection = false,
|
enableSelection = false,
|
||||||
bottomSpacing = false,
|
bottomSpacing = false,
|
||||||
showAllBlocks = false,
|
showAllBlocks = false,
|
||||||
|
showToday = true,
|
||||||
quickAdd,
|
quickAdd,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -71,6 +73,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
||||||
bottomSpacing={bottomSpacing}
|
bottomSpacing={bottomSpacing}
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
quickAdd={quickAdd}
|
quickAdd={quickAdd}
|
||||||
|
showToday={showToday}
|
||||||
/>
|
/>
|
||||||
</GanttStoreProvider>
|
</GanttStoreProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ type Props = {
|
||||||
canLoadMoreBlocks?: boolean;
|
canLoadMoreBlocks?: boolean;
|
||||||
loadMoreBlocks?: () => void;
|
loadMoreBlocks?: () => void;
|
||||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean | ((blockId: string) => boolean);
|
||||||
enableSelection: boolean;
|
enableSelection: boolean | ((blockId: string) => boolean);
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
|
||||||
|
|
@ -94,7 +94,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||||
canLoadMoreBlocks,
|
canLoadMoreBlocks,
|
||||||
ganttContainerRef,
|
ganttContainerRef,
|
||||||
loadMoreBlocks,
|
loadMoreBlocks,
|
||||||
selectionHelpers
|
selectionHelpers,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{quickAdd ? quickAdd : null}
|
{quickAdd ? quickAdd : null}
|
||||||
|
|
|
||||||
|
|
@ -1,405 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, FC, ChangeEvent } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useForm, Controller } from "react-hook-form";
|
|
||||||
import { Info, X } from "lucide-react";
|
|
||||||
import { IProject } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
CustomEmojiIconPicker,
|
|
||||||
CustomSelect,
|
|
||||||
EmojiIconPickerTypes,
|
|
||||||
Input,
|
|
||||||
setToast,
|
|
||||||
TextArea,
|
|
||||||
TOAST_TYPE,
|
|
||||||
Tooltip,
|
|
||||||
} from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { Logo } from "@/components/common";
|
|
||||||
import { ImagePickerPopover } from "@/components/core";
|
|
||||||
import { MemberDropdown } from "@/components/dropdowns";
|
|
||||||
// constants
|
|
||||||
import { PROJECT_CREATED } from "@/constants/event-tracker";
|
|
||||||
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "@/constants/project";
|
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { convertHexEmojiToDecimal, getRandomEmoji } from "@/helpers/emoji.helper";
|
|
||||||
import { projectIdentifierSanitizer } from "@/helpers/project.helper";
|
|
||||||
// hooks
|
|
||||||
import { useEventTracker, useProject } from "@/hooks/store";
|
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setToFavorite?: boolean;
|
|
||||||
workspaceSlug: string;
|
|
||||||
onClose: () => void;
|
|
||||||
handleNextStep: (projectId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
|
||||||
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 [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
} = useForm<IProject>({
|
|
||||||
defaultValues,
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
});
|
|
||||||
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<IProject>) => {
|
|
||||||
// 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 handleNameChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!isChangeInIdentifierRequired) {
|
|
||||||
onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.target.value === "") setValue("identifier", "");
|
|
||||||
else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5));
|
|
||||||
onChange(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = e.target;
|
|
||||||
const alphanumericValue = projectIdentifierSanitizer(value);
|
|
||||||
setIsChangeInIdentifierRequired(false);
|
|
||||||
onChange(alphanumericValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
setIsChangeInIdentifierRequired(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
reset();
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="group relative h-44 w-full rounded-lg bg-custom-background-80">
|
|
||||||
{watch("cover_image") && (
|
|
||||||
<img
|
|
||||||
src={watch("cover_image")!}
|
|
||||||
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
|
||||||
alt="Cover image"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="absolute right-2 top-2 p-2">
|
|
||||||
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={8}>
|
|
||||||
<X className="h-5 w-5 text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-2 right-2">
|
|
||||||
<Controller
|
|
||||||
name="cover_image"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<ImagePickerPopover
|
|
||||||
label="Change Cover"
|
|
||||||
onChange={onChange}
|
|
||||||
control={control}
|
|
||||||
value={value}
|
|
||||||
tabIndex={9}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute -bottom-[22px] left-3">
|
|
||||||
<Controller
|
|
||||||
name="logo_props"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<CustomEmojiIconPicker
|
|
||||||
isOpen={isOpen}
|
|
||||||
handleToggle={(val: boolean) => setIsOpen(val)}
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
buttonClassName="flex items-center justify-center"
|
|
||||||
label={
|
|
||||||
<span className="grid h-11 w-11 place-items-center rounded-md bg-custom-background-80">
|
|
||||||
<Logo logo={value} size={20} />
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
onChange={(val: any) => {
|
|
||||||
let logoValue = {};
|
|
||||||
|
|
||||||
if (val?.type === "emoji")
|
|
||||||
logoValue = {
|
|
||||||
value: convertHexEmojiToDecimal(val.value.unified),
|
|
||||||
url: val.value.imageUrl,
|
|
||||||
};
|
|
||||||
else if (val?.type === "icon") logoValue = val.value;
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
in_use: val?.type,
|
|
||||||
[val?.type]: logoValue,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined}
|
|
||||||
defaultOpen={
|
|
||||||
value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="px-3">
|
|
||||||
<div className="mt-9 space-y-6 pb-5">
|
|
||||||
<div className="grid grid-cols-1 gap-x-2 gap-y-3 md:grid-cols-4">
|
|
||||||
<div className="md:col-span-3">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
rules={{
|
|
||||||
required: "Name is required",
|
|
||||||
maxLength: {
|
|
||||||
value: 255,
|
|
||||||
message: "Title should be less than 255 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={handleNameChange(onChange)}
|
|
||||||
hasError={Boolean(errors.name)}
|
|
||||||
placeholder="Project name"
|
|
||||||
className="w-full focus:border-blue-400"
|
|
||||||
tabIndex={1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-red-500">
|
|
||||||
<>{errors?.name?.message}</>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="identifier"
|
|
||||||
rules={{
|
|
||||||
required: "Project ID is required",
|
|
||||||
// allow only alphanumeric & non-latin characters
|
|
||||||
validate: (value) =>
|
|
||||||
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) ||
|
|
||||||
"Only Alphanumeric & Non-latin characters are allowed.",
|
|
||||||
minLength: {
|
|
||||||
value: 1,
|
|
||||||
message: "Project ID must at least be of 1 character",
|
|
||||||
},
|
|
||||||
maxLength: {
|
|
||||||
value: 5,
|
|
||||||
message: "Project ID must at most be of 5 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Input
|
|
||||||
id="identifier"
|
|
||||||
name="identifier"
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={handleIdentifierChange(onChange)}
|
|
||||||
hasError={Boolean(errors.identifier)}
|
|
||||||
placeholder="Project ID"
|
|
||||||
className={cn("w-full text-xs focus:border-blue-400 pr-7", {
|
|
||||||
uppercase: value,
|
|
||||||
})}
|
|
||||||
tabIndex={2}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
isMobile={isMobile}
|
|
||||||
tooltipContent="Helps you identify issues in the project uniquely. Max 5 characters."
|
|
||||||
className="text-sm"
|
|
||||||
position="right-top"
|
|
||||||
>
|
|
||||||
<Info className="absolute right-2 top-2.5 h-3 w-3 text-custom-text-400" />
|
|
||||||
</Tooltip>
|
|
||||||
<span className="text-xs text-red-500">{errors?.identifier?.message}</span>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-4">
|
|
||||||
<Controller
|
|
||||||
name="description"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value={value}
|
|
||||||
placeholder="Description..."
|
|
||||||
onChange={onChange}
|
|
||||||
className="!h-24 text-sm focus:border-blue-400"
|
|
||||||
hasError={Boolean(errors?.description)}
|
|
||||||
tabIndex={3}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t border-custom-border-100">
|
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={6}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}>
|
|
||||||
{isSubmitting ? "Creating" : "Create project"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useEffect, Fragment, FC, useState } from "react";
|
import { useEffect, Fragment, FC, useState } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { CreateProjectForm } from "./create-project-form";
|
import { CreateProjectForm } from "@/plane-web/components/projects/create/root";
|
||||||
|
import { TProject } from "@/plane-web/types/projects";
|
||||||
import { ProjectFeatureUpdate } from "./project-feature-update";
|
import { ProjectFeatureUpdate } from "./project-feature-update";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -9,6 +10,7 @@ type Props = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
setToFavorite?: boolean;
|
setToFavorite?: boolean;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
data?: Partial<TProject>;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum EProjectCreationSteps {
|
enum EProjectCreationSteps {
|
||||||
|
|
@ -17,7 +19,7 @@ enum EProjectCreationSteps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateProjectModal: FC<Props> = (props) => {
|
export const CreateProjectModal: FC<Props> = (props) => {
|
||||||
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
|
const { isOpen, onClose, setToFavorite = false, workspaceSlug, data } = props;
|
||||||
// states
|
// states
|
||||||
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
const [currentStep, setCurrentStep] = useState<EProjectCreationSteps>(EProjectCreationSteps.CREATE_PROJECT);
|
||||||
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
const [createdProjectId, setCreatedProjectId] = useState<string | null>(null);
|
||||||
|
|
@ -68,6 +70,7 @@ export const CreateProjectModal: FC<Props> = (props) => {
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
handleNextStep={handleNextStep}
|
handleNextStep={handleNextStep}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === EProjectCreationSteps.FEATURE_SELECTION && (
|
{currentStep === EProjectCreationSteps.FEATURE_SELECTION && (
|
||||||
|
|
|
||||||
133
web/core/components/project/create/common-attributes.tsx
Normal file
133
web/core/components/project/create/common-attributes.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
import { Controller, useFormContext, UseFormSetValue } from "react-hook-form";
|
||||||
|
import { Info } from "lucide-react";
|
||||||
|
import { cn } from "@plane/editor";
|
||||||
|
import { Input, TextArea, Tooltip } from "@plane/ui";
|
||||||
|
import { projectIdentifierSanitizer } from "@/helpers/project.helper";
|
||||||
|
import { TProject } from "@/plane-web/types/projects";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setValue: UseFormSetValue<TProject>;
|
||||||
|
isMobile: boolean;
|
||||||
|
isChangeInIdentifierRequired: boolean;
|
||||||
|
setIsChangeInIdentifierRequired: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
const ProjectCommonAttributes: React.FC<Props> = (props) => {
|
||||||
|
const { setValue, isMobile, isChangeInIdentifierRequired, setIsChangeInIdentifierRequired } = props;
|
||||||
|
const {
|
||||||
|
formState: { errors },
|
||||||
|
control,
|
||||||
|
} = useFormContext<TProject>();
|
||||||
|
|
||||||
|
const handleNameChange = (onChange: (...event: any[]) => void) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!isChangeInIdentifierRequired) {
|
||||||
|
onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.target.value === "") setValue("identifier", "");
|
||||||
|
else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5));
|
||||||
|
onChange(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
const alphanumericValue = projectIdentifierSanitizer(value);
|
||||||
|
setIsChangeInIdentifierRequired(false);
|
||||||
|
onChange(alphanumericValue);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-x-2 gap-y-3 md:grid-cols-4">
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
rules={{
|
||||||
|
required: "Name is required",
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: "Title should be less than 255 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={handleNameChange(onChange)}
|
||||||
|
hasError={Boolean(errors.name)}
|
||||||
|
placeholder="Project name"
|
||||||
|
className="w-full focus:border-blue-400"
|
||||||
|
tabIndex={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="identifier"
|
||||||
|
rules={{
|
||||||
|
required: "Project ID is required",
|
||||||
|
// allow only alphanumeric & non-latin characters
|
||||||
|
validate: (value) =>
|
||||||
|
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || "Only Alphanumeric & Non-latin characters are allowed.",
|
||||||
|
minLength: {
|
||||||
|
value: 1,
|
||||||
|
message: "Project ID must at least be of 1 character",
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 5,
|
||||||
|
message: "Project ID must at most be of 5 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
id="identifier"
|
||||||
|
name="identifier"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={handleIdentifierChange(onChange)}
|
||||||
|
hasError={Boolean(errors.identifier)}
|
||||||
|
placeholder="Project ID"
|
||||||
|
className={cn("w-full text-xs focus:border-blue-400 pr-7", {
|
||||||
|
uppercase: value,
|
||||||
|
})}
|
||||||
|
tabIndex={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
|
tooltipContent="Helps you identify issues in the project uniquely. Max 5 characters."
|
||||||
|
className="text-sm"
|
||||||
|
position="right-top"
|
||||||
|
>
|
||||||
|
<Info className="absolute right-2 top-2.5 h-3 w-3 text-custom-text-400" />
|
||||||
|
</Tooltip>
|
||||||
|
<span className="text-xs text-red-500">{errors?.identifier?.message}</span>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-4">
|
||||||
|
<Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={value}
|
||||||
|
placeholder="Description..."
|
||||||
|
onChange={onChange}
|
||||||
|
className="!h-24 text-sm focus:border-blue-400"
|
||||||
|
hasError={Boolean(errors?.description)}
|
||||||
|
tabIndex={3}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCommonAttributes;
|
||||||
85
web/core/components/project/create/header.tsx
Normal file
85
web/core/components/project/create/header.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { IProject } from "@plane/types";
|
||||||
|
import { CustomEmojiIconPicker, EmojiIconPickerTypes, Logo } from "@plane/ui";
|
||||||
|
import { ImagePickerPopover } from "@/components/core";
|
||||||
|
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
};
|
||||||
|
const ProjectCreateHeader: React.FC<Props> = (props) => {
|
||||||
|
const { handleClose } = props;
|
||||||
|
const { watch, control } = useFormContext<IProject>();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group relative h-44 w-full rounded-lg bg-custom-background-80">
|
||||||
|
{watch("cover_image") && (
|
||||||
|
<img
|
||||||
|
src={watch("cover_image")!}
|
||||||
|
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
|
||||||
|
alt="Cover image"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="absolute right-2 top-2 p-2">
|
||||||
|
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={8}>
|
||||||
|
<X className="h-5 w-5 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 right-2">
|
||||||
|
<Controller
|
||||||
|
name="cover_image"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<ImagePickerPopover label="Change Cover" onChange={onChange} control={control} value={value} tabIndex={9} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-[22px] left-3">
|
||||||
|
<Controller
|
||||||
|
name="logo_props"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomEmojiIconPicker
|
||||||
|
isOpen={isOpen}
|
||||||
|
handleToggle={(val: boolean) => setIsOpen(val)}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
buttonClassName="flex items-center justify-center"
|
||||||
|
label={
|
||||||
|
<span className="grid h-11 w-11 place-items-center rounded-md bg-custom-background-80">
|
||||||
|
<Logo logo={value} size={20} />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
onChange={(val: any) => {
|
||||||
|
let logoValue = {};
|
||||||
|
|
||||||
|
if (val?.type === "emoji")
|
||||||
|
logoValue = {
|
||||||
|
value: convertHexEmojiToDecimal(val.value.unified),
|
||||||
|
url: val.value.imageUrl,
|
||||||
|
};
|
||||||
|
else if (val?.type === "icon") logoValue = val.value;
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
in_use: val?.type,
|
||||||
|
[val?.type]: logoValue,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined}
|
||||||
|
defaultOpen={
|
||||||
|
value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCreateHeader;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { IProject } from "@plane/types";
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
};
|
||||||
|
const ProjectCreateButtons: React.FC<Props> = (props) => {
|
||||||
|
const { handleClose } = props;
|
||||||
|
const {
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useFormContext<IProject>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end gap-2 pt-4 border-t border-custom-border-100">
|
||||||
|
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={6}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}>
|
||||||
|
{isSubmitting ? "Creating" : "Create project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCreateButtons;
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { Search, Briefcase, X, ListFilter } from "lucide-react";
|
import { Search, Briefcase, X, ListFilter } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TProjectFilters } from "@plane/types";
|
import { TProjectFilters } from "@plane/types";
|
||||||
|
|
@ -21,7 +21,7 @@ import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
|
|
||||||
export const ProjectsListHeader = observer(() => {
|
export const ProjectsBaseHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
// states
|
// states
|
||||||
|
|
@ -34,6 +34,8 @@ export const ProjectsListHeader = observer(() => {
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentWorkspaceDisplayFilters: displayFilters,
|
currentWorkspaceDisplayFilters: displayFilters,
|
||||||
currentWorkspaceFilters: filters,
|
currentWorkspaceFilters: filters,
|
||||||
|
|
@ -51,6 +53,7 @@ export const ProjectsListHeader = observer(() => {
|
||||||
});
|
});
|
||||||
// auth
|
// auth
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
const isArchived = pathname.includes("/archives");
|
||||||
|
|
||||||
const handleFilters = useCallback(
|
const handleFilters = useCallback(
|
||||||
(key: keyof TProjectFilters, value: string | string[]) => {
|
(key: keyof TProjectFilters, value: string | string[]) => {
|
||||||
|
|
@ -97,6 +100,7 @@ export const ProjectsListHeader = observer(() => {
|
||||||
type="text"
|
type="text"
|
||||||
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
|
||||||
/>
|
/>
|
||||||
|
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5,7 +5,6 @@ export * from "./settings";
|
||||||
export * from "./card-list";
|
export * from "./card-list";
|
||||||
export * from "./card";
|
export * from "./card";
|
||||||
export * from "./create-project-modal";
|
export * from "./create-project-modal";
|
||||||
export * from "./create-project-form";
|
|
||||||
export * from "./project-feature-update";
|
export * from "./project-feature-update";
|
||||||
export * from "./delete-project-modal";
|
export * from "./delete-project-modal";
|
||||||
export * from "./form-loader";
|
export * from "./form-loader";
|
||||||
|
|
@ -19,3 +18,4 @@ export * from "./member-list-item";
|
||||||
export * from "./project-settings-member-defaults";
|
export * from "./project-settings-member-defaults";
|
||||||
export * from "./send-project-invitation-modal";
|
export * from "./send-project-invitation-modal";
|
||||||
export * from "./confirm-project-member-remove";
|
export * from "./confirm-project-member-remove";
|
||||||
|
export * from "@/plane-web/components/projects/create/root";
|
||||||
|
|
|
||||||
90
web/core/components/project/root.tsx
Normal file
90
web/core/components/project/root.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// types
|
||||||
|
import { useParams, usePathname } from "next/navigation";
|
||||||
|
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 Root = observer(() => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { workspaceSlug } = useParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
// store
|
||||||
|
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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname.includes("/archives")) {
|
||||||
|
updateDisplayFilters(workspaceSlug.toString(), { archived_projects: true });
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Root;
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ArchiveIcon, ChevronRight, MoreHorizontal, Settings } from "lucide-react";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { SidebarNavItem } from "@/components/sidebar";
|
import { SidebarNavItem } from "@/components/sidebar";
|
||||||
// constants
|
// constants
|
||||||
|
|
@ -19,11 +19,16 @@ import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
|
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
|
||||||
import useLocalStorage from "@/hooks/use-local-storage";
|
import useLocalStorage from "@/hooks/use-local-storage";
|
||||||
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { UpgradeBadge } from "@/plane-web/components/workspace";
|
import { UpgradeBadge } from "@/plane-web/components/workspace";
|
||||||
|
|
||||||
export const SidebarWorkspaceMenu = observer(() => {
|
export const SidebarWorkspaceMenu = observer(() => {
|
||||||
|
// state
|
||||||
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
// refs
|
||||||
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
|
|
@ -54,6 +59,7 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sidebarCollapsed) toggleWorkspaceMenu(true);
|
if (sidebarCollapsed) toggleWorkspaceMenu(true);
|
||||||
}, [sidebarCollapsed, toggleWorkspaceMenu]);
|
}, [sidebarCollapsed, toggleWorkspaceMenu]);
|
||||||
|
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
const indicatorElement = (
|
const indicatorElement = (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|
@ -64,20 +70,70 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||||
return (
|
return (
|
||||||
<Disclosure as="div" defaultOpen>
|
<Disclosure as="div" defaultOpen>
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<Disclosure.Button
|
<div className="flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded">
|
||||||
as="button"
|
{" "}
|
||||||
className="sticky top-0 bg-custom-sidebar-background-100 z-10 group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
<Disclosure.Button
|
||||||
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
as="button"
|
||||||
>
|
className="flex-1 sticky top-0 z-10 w-full py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 text-xs font-semibold"
|
||||||
<span>WORKSPACE</span>
|
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
||||||
<span className="flex-shrink-0 opacity-0 pointer-events-none group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto rounded p-0.5 hover:bg-custom-sidebar-background-80">
|
>
|
||||||
<ChevronRight
|
<span>WORKSPACE</span>
|
||||||
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
</Disclosure.Button>
|
||||||
"rotate-90": isWorkspaceMenuOpen,
|
<CustomMenu
|
||||||
})}
|
customButton={
|
||||||
/>
|
<span
|
||||||
</span>
|
ref={actionSectionRef}
|
||||||
</Disclosure.Button>
|
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded my-auto"
|
||||||
|
onClick={() => {
|
||||||
|
console.log("ndkn");
|
||||||
|
setIsMenuActive(!isMenuActive);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": isMenuActive,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
customButtonClassName="grid place-items-center"
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem>
|
||||||
|
<Link href={`/${workspaceSlug}/projects/archives`}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
|
<span>Archives</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
|
||||||
|
<CustomMenu.MenuItem>
|
||||||
|
<Link href={`/${workspaceSlug}/settings`}>
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
<Disclosure.Button
|
||||||
|
as="button"
|
||||||
|
className="sticky top-0 z-10 group/workspace-button px-0.5 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
|
||||||
|
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<span className="flex-shrink-0 opacity-0 pointer-events-none group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto rounded hover:bg-custom-sidebar-background-80">
|
||||||
|
<ChevronRight
|
||||||
|
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
|
||||||
|
"rotate-90": isWorkspaceMenuOpen,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Disclosure.Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Transition
|
<Transition
|
||||||
show={isWorkspaceMenuOpen}
|
show={isWorkspaceMenuOpen}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import sortBy from "lodash/sortBy";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// types
|
||||||
import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types";
|
import {
|
||||||
|
IProjectBulkAddFormData,
|
||||||
|
IProjectMember,
|
||||||
|
IProjectMemberLite,
|
||||||
|
IProjectMembership,
|
||||||
|
IUserLite,
|
||||||
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
// services
|
// services
|
||||||
|
|
@ -12,6 +18,7 @@ import { ProjectMemberService } from "@/services/project";
|
||||||
import { IRouterStore } from "@/store/router.store";
|
import { IRouterStore } from "@/store/router.store";
|
||||||
import { IUserStore } from "@/store/user";
|
import { IUserStore } from "@/store/user";
|
||||||
// store
|
// store
|
||||||
|
import { IProjectStore } from "../project/project.store";
|
||||||
import { CoreRootStore } from "../root.store";
|
import { CoreRootStore } from "../root.store";
|
||||||
import { IMemberRootStore } from ".";
|
import { IMemberRootStore } from ".";
|
||||||
|
|
||||||
|
|
@ -58,6 +65,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
routerStore: IRouterStore;
|
routerStore: IRouterStore;
|
||||||
userStore: IUserStore;
|
userStore: IUserStore;
|
||||||
memberRoot: IMemberRootStore;
|
memberRoot: IMemberRootStore;
|
||||||
|
projectRoot: IProjectStore;
|
||||||
// services
|
// services
|
||||||
projectMemberService;
|
projectMemberService;
|
||||||
|
|
||||||
|
|
@ -78,6 +86,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
this.routerStore = _rootStore.router;
|
this.routerStore = _rootStore.router;
|
||||||
this.userStore = _rootStore.user;
|
this.userStore = _rootStore.user;
|
||||||
this.memberRoot = _memberRoot;
|
this.memberRoot = _memberRoot;
|
||||||
|
this.projectRoot = _rootStore.projectRoot.project;
|
||||||
// services
|
// services
|
||||||
this.projectMemberService = new ProjectMemberService();
|
this.projectMemberService = new ProjectMemberService();
|
||||||
}
|
}
|
||||||
|
|
@ -159,6 +168,10 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
set(this.projectMemberMap, [projectId, member.member], member);
|
set(this.projectMemberMap, [projectId, member.member], member);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members.concat(
|
||||||
|
data.members as unknown as IProjectMemberLite[]
|
||||||
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -212,6 +225,9 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
delete this.projectMemberMap?.[projectId]?.[userId];
|
delete this.projectMemberMap?.[projectId]?.[userId];
|
||||||
});
|
});
|
||||||
|
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members.filter(
|
||||||
|
(member) => member.id !== userId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@ import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
|
||||||
import { IProject } from "@plane/types";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { orderProjects, shouldFilterProject } from "@/helpers/project.helper";
|
import { orderProjects, shouldFilterProject } from "@/helpers/project.helper";
|
||||||
// services
|
// services
|
||||||
|
import { TProject } from "@/plane-web/types/projects/projects";
|
||||||
import { IssueLabelService, IssueService } from "@/services/issue";
|
import { IssueLabelService, IssueService } from "@/services/issue";
|
||||||
import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project";
|
import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project";
|
||||||
// store
|
// store
|
||||||
|
|
@ -16,7 +15,7 @@ export interface IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
projectMap: {
|
projectMap: {
|
||||||
[projectId: string]: IProject; // projectId: project Info
|
[projectId: string]: TProject; // projectId: project Info
|
||||||
};
|
};
|
||||||
// computed
|
// computed
|
||||||
filteredProjectIds: string[] | undefined;
|
filteredProjectIds: string[] | undefined;
|
||||||
|
|
@ -25,21 +24,21 @@ export interface IProjectStore {
|
||||||
totalProjectIds: string[] | undefined;
|
totalProjectIds: string[] | undefined;
|
||||||
joinedProjectIds: string[];
|
joinedProjectIds: string[];
|
||||||
favoriteProjectIds: string[];
|
favoriteProjectIds: string[];
|
||||||
currentProjectDetails: IProject | undefined;
|
currentProjectDetails: TProject | undefined;
|
||||||
// actions
|
// actions
|
||||||
getProjectById: (projectId: string | undefined | null) => IProject | undefined;
|
getProjectById: (projectId: string | undefined | null) => TProject | undefined;
|
||||||
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
getProjectIdentifierById: (projectId: string | undefined | null) => string;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjects: (workspaceSlug: string) => Promise<IProject[]>;
|
fetchProjects: (workspaceSlug: string) => Promise<TProject[]>;
|
||||||
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<IProject>;
|
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<TProject>;
|
||||||
// favorites actions
|
// favorites actions
|
||||||
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
// project-view action
|
// project-view action
|
||||||
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
||||||
// CRUD actions
|
// CRUD actions
|
||||||
createProject: (workspaceSlug: string, data: Partial<IProject>) => Promise<IProject>;
|
createProject: (workspaceSlug: string, data: Partial<TProject>) => Promise<TProject>;
|
||||||
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<IProject>;
|
updateProject: (workspaceSlug: string, projectId: string, data: Partial<TProject>) => Promise<TProject>;
|
||||||
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
// archive actions
|
// archive actions
|
||||||
archiveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
archiveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
|
|
@ -50,7 +49,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
loader: boolean = false;
|
loader: boolean = false;
|
||||||
projectMap: {
|
projectMap: {
|
||||||
[projectId: string]: IProject; // projectId: project Info
|
[projectId: string]: TProject; // projectId: project Info
|
||||||
} = {};
|
} = {};
|
||||||
// root store
|
// root store
|
||||||
rootStore: CoreRootStore;
|
rootStore: CoreRootStore;
|
||||||
|
|
@ -206,7 +205,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
/**
|
/**
|
||||||
* get Workspace projects using workspace slug
|
* get Workspace projects using workspace slug
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @returns Promise<IProject[]>
|
* @returns Promise<TProject[]>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
fetchProjects = async (workspaceSlug: string) => {
|
fetchProjects = async (workspaceSlug: string) => {
|
||||||
|
|
@ -231,7 +230,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
* Fetches project details using workspace slug and project id
|
* Fetches project details using workspace slug and project id
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param projectId
|
* @param projectId
|
||||||
* @returns Promise<IProject>
|
* @returns Promise<TProject>
|
||||||
*/
|
*/
|
||||||
fetchProjectDetails = async (workspaceSlug: string, projectId: string) => {
|
fetchProjectDetails = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -249,7 +248,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
/**
|
/**
|
||||||
* Returns project details using project id
|
* Returns project details using project id
|
||||||
* @param projectId
|
* @param projectId
|
||||||
* @returns IProject | null
|
* @returns TProject | null
|
||||||
*/
|
*/
|
||||||
getProjectById = computedFn((projectId: string | undefined | null) => {
|
getProjectById = computedFn((projectId: string | undefined | null) => {
|
||||||
const projectInfo = this.projectMap[projectId ?? ""] || undefined;
|
const projectInfo = this.projectMap[projectId ?? ""] || undefined;
|
||||||
|
|
@ -348,7 +347,7 @@ export class ProjectStore implements IProjectStore {
|
||||||
* Creates a project in the workspace and adds it to the store
|
* Creates a project in the workspace and adds it to the store
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param data
|
* @param data
|
||||||
* @returns Promise<IProject>
|
* @returns Promise<TProject>
|
||||||
*/
|
*/
|
||||||
createProject = async (workspaceSlug: string, data: any) => {
|
createProject = async (workspaceSlug: string, data: any) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -369,9 +368,9 @@ export class ProjectStore implements IProjectStore {
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param projectId
|
* @param projectId
|
||||||
* @param data
|
* @param data
|
||||||
* @returns Promise<IProject>
|
* @returns Promise<TProject>
|
||||||
*/
|
*/
|
||||||
updateProject = async (workspaceSlug: string, projectId: string, data: Partial<IProject>) => {
|
updateProject = async (workspaceSlug: string, projectId: string, data: Partial<TProject>) => {
|
||||||
try {
|
try {
|
||||||
const projectDetails = this.getProjectById(projectId);
|
const projectDetails = this.getProjectById(projectId);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
|
||||||
1
web/ee/components/projects/create/attributes.tsx
Normal file
1
web/ee/components/projects/create/attributes.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/projects/create/attributes";
|
||||||
1
web/ee/components/projects/create/root.tsx
Normal file
1
web/ee/components/projects/create/root.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/projects/create/root";
|
||||||
1
web/ee/components/projects/header.tsx
Normal file
1
web/ee/components/projects/header.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/projects/header";
|
||||||
1
web/ee/components/projects/mobile-header.tsx
Normal file
1
web/ee/components/projects/mobile-header.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/projects/mobile-header";
|
||||||
1
web/ee/components/projects/page.tsx
Normal file
1
web/ee/components/projects/page.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/projects/page";
|
||||||
1
web/ee/types/projects/projects.ts
Normal file
1
web/ee/types/projects/projects.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/types/projects/projects";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue