mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: app dir build errors. (#4746)
This commit is contained in:
parent
f93803ace8
commit
c880e8b48c
@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
|
@ -20,7 +20,7 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
|
|||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
|
|
||||||
export enum EOnboardingSteps {
|
enum EOnboardingSteps {
|
||||||
PROFILE_SETUP = "PROFILE_SETUP",
|
PROFILE_SETUP = "PROFILE_SETUP",
|
||||||
WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN",
|
WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN",
|
||||||
INVITE_MEMBERS = "INVITE_MEMBERS",
|
INVITE_MEMBERS = "INVITE_MEMBERS",
|
||||||
|
@ -33,8 +33,8 @@ const defaultValues: FormValues = {
|
|||||||
confirm_password: "",
|
confirm_password: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userService = new UserService();
|
const userService = new UserService();
|
||||||
export const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
||||||
const defaultShowPassword = {
|
const defaultShowPassword = {
|
||||||
oldPassword: false,
|
oldPassword: false,
|
||||||
|
@ -1,291 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
// types
|
|
||||||
import { IEstimate, IEstimateFormData } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
|
|
||||||
// helpers
|
|
||||||
import { checkDuplicates } from "@/helpers/array.helper";
|
|
||||||
// hooks
|
|
||||||
import { useEstimate } from "@/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
data?: IEstimate;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
value1: "",
|
|
||||||
value2: "",
|
|
||||||
value3: "",
|
|
||||||
value4: "",
|
|
||||||
value5: "",
|
|
||||||
value6: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormValues = typeof defaultValues;
|
|
||||||
|
|
||||||
export const CreateUpdateEstimateModal: React.FC<Props> = observer((props) => {
|
|
||||||
const { handleClose, data, isOpen } = props;
|
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const { createEstimate, updateEstimate } = useEstimate();
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
} = useForm<FormValues>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
handleClose();
|
|
||||||
reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateEstimate = async (payload: IEstimateFormData) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
await createEstimate(workspaceSlug.toString(), projectId.toString(), payload)
|
|
||||||
.then(() => {
|
|
||||||
onClose();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const error = err?.error;
|
|
||||||
const errorString = Array.isArray(error) ? error[0] : error;
|
|
||||||
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message:
|
|
||||||
errorString ?? err.status === 400
|
|
||||||
? "Estimate with that name already exists. Please try again with another name."
|
|
||||||
: "Estimate could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateEstimate = async (payload: IEstimateFormData) => {
|
|
||||||
if (!workspaceSlug || !projectId || !data) return;
|
|
||||||
|
|
||||||
await updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload)
|
|
||||||
.then(() => {
|
|
||||||
onClose();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const error = err?.error;
|
|
||||||
const errorString = Array.isArray(error) ? error[0] : error;
|
|
||||||
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: errorString ?? "Estimate could not be updated. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (formData: FormValues) => {
|
|
||||||
if (!formData.name || formData.name === "") {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate title cannot be empty.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
formData.value1 === "" ||
|
|
||||||
formData.value2 === "" ||
|
|
||||||
formData.value3 === "" ||
|
|
||||||
formData.value4 === "" ||
|
|
||||||
formData.value5 === "" ||
|
|
||||||
formData.value6 === ""
|
|
||||||
) {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate point cannot be empty.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
formData.value1.length > 20 ||
|
|
||||||
formData.value2.length > 20 ||
|
|
||||||
formData.value3.length > 20 ||
|
|
||||||
formData.value4.length > 20 ||
|
|
||||||
formData.value5.length > 20 ||
|
|
||||||
formData.value6.length > 20
|
|
||||||
) {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate point cannot have more than 20 characters.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
checkDuplicates([
|
|
||||||
formData.value1,
|
|
||||||
formData.value2,
|
|
||||||
formData.value3,
|
|
||||||
formData.value4,
|
|
||||||
formData.value5,
|
|
||||||
formData.value6,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Estimate points cannot have duplicate values.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: IEstimateFormData = {
|
|
||||||
estimate: {
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
},
|
|
||||||
estimate_points: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
const point = {
|
|
||||||
key: i,
|
|
||||||
value: formData[`value${i + 1}` as keyof FormValues],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data)
|
|
||||||
payload.estimate_points.push({
|
|
||||||
id: data.points[i].id,
|
|
||||||
...point,
|
|
||||||
});
|
|
||||||
else payload.estimate_points.push({ ...point });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) await handleUpdateEstimate(payload);
|
|
||||||
else await handleCreateEstimate(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data)
|
|
||||||
reset({
|
|
||||||
...defaultValues,
|
|
||||||
...data,
|
|
||||||
value1: data.points[0]?.value,
|
|
||||||
value2: data.points[1]?.value,
|
|
||||||
value3: data.points[2]?.value,
|
|
||||||
value4: data.points[3]?.value,
|
|
||||||
value5: data.points[4]?.value,
|
|
||||||
value6: data.points[5]?.value,
|
|
||||||
});
|
|
||||||
else reset({ ...defaultValues });
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="space-y-5 p-5">
|
|
||||||
<div className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Create"} Estimate</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="name"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref}
|
|
||||||
hasError={Boolean(errors.name)}
|
|
||||||
placeholder="Title"
|
|
||||||
className="w-full text-base"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="description"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value={value}
|
|
||||||
placeholder="Description"
|
|
||||||
onChange={onChange}
|
|
||||||
className="w-full text-base resize-none min-h-24"
|
|
||||||
hasError={Boolean(errors?.description)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* list of all the points */}
|
|
||||||
{/* since they are all the same, we can use a loop to render them */}
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{Array(6)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => (
|
|
||||||
<div className="flex items-center" key={i}>
|
|
||||||
<span className="flex h-full items-center rounded-lg bg-custom-background-80">
|
|
||||||
<span className="rounded-lg px-2 text-sm text-custom-text-200">{i + 1}</span>
|
|
||||||
<span className="rounded-r-lg bg-custom-background-100">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`value${i + 1}` as keyof FormValues}
|
|
||||||
rules={{
|
|
||||||
maxLength: {
|
|
||||||
value: 20,
|
|
||||||
message: "Estimate point must at most be of 20 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
ref={ref}
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
id={`value${i + 1}`}
|
|
||||||
name={`value${i + 1}`}
|
|
||||||
placeholder={`Point ${i + 1}`}
|
|
||||||
className="w-full rounded-l-none"
|
|
||||||
hasError={Boolean(errors[`value${i + 1}` as keyof FormValues])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
|
||||||
{data ? (isSubmitting ? "Updating" : "Update Estimate") : isSubmitting ? "Creating" : "Create Estimate"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalCore>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,78 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
// types
|
|
||||||
import { IEstimate } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { AlertModalCore } from "@/components/core";
|
|
||||||
// hooks
|
|
||||||
import { useEstimate } from "@/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
data: IEstimate | null;
|
|
||||||
handleClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
|
|
||||||
const { isOpen, handleClose, data } = props;
|
|
||||||
// states
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const { deleteEstimate } = useEstimate();
|
|
||||||
|
|
||||||
const handleEstimateDelete = async () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
||||||
const estimateId = data?.id!;
|
|
||||||
|
|
||||||
await deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId)
|
|
||||||
.then(() => {
|
|
||||||
handleClose();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const error = err?.error;
|
|
||||||
const errorString = Array.isArray(error) ? error[0] : error;
|
|
||||||
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: errorString ?? "Estimate could not be deleted. Please try again",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => setIsDeleteLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertModalCore
|
|
||||||
handleClose={onClose}
|
|
||||||
handleSubmit={handleEstimateDelete}
|
|
||||||
isSubmitting={isDeleteLoading}
|
|
||||||
isOpen={isOpen}
|
|
||||||
title="Delete Estimate"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
Are you sure you want to delete estimate-{" "}
|
|
||||||
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>
|
|
||||||
{""}? All of the data related to the estiamte will be permanently removed. This action cannot be undone.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,120 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { IEstimate } from "@plane/types";
|
|
||||||
// store hooks
|
|
||||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
import { EmptyState } from "@/components/empty-state";
|
|
||||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "@/components/estimates";
|
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
|
||||||
import { orderArrayBy } from "@/helpers/array.helper";
|
|
||||||
import { useEstimate, useProject } from "@/hooks/store";
|
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// types
|
|
||||||
// helpers
|
|
||||||
// constants
|
|
||||||
|
|
||||||
export const EstimatesList: React.FC = observer(() => {
|
|
||||||
// states
|
|
||||||
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
|
|
||||||
const [estimateToDelete, setEstimateToDelete] = useState<string | null>(null);
|
|
||||||
const [estimateToUpdate, setEstimateToUpdate] = useState<IEstimate | undefined>();
|
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const { updateProject, currentProjectDetails } = useProject();
|
|
||||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
|
||||||
|
|
||||||
const editEstimate = (estimate: IEstimate) => {
|
|
||||||
setEstimateFormOpen(true);
|
|
||||||
// Order the points array by key before updating the estimate to update state
|
|
||||||
setEstimateToUpdate({
|
|
||||||
...estimate,
|
|
||||||
points: orderArrayBy(estimate.points, "key"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableEstimates = () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
updateProject(workspaceSlug.toString(), projectId.toString(), { estimate: null }).catch((err) => {
|
|
||||||
const error = err?.error;
|
|
||||||
const errorString = Array.isArray(error) ? error[0] : error;
|
|
||||||
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: errorString ?? "Estimate could not be disabled. Please try again",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CreateUpdateEstimateModal
|
|
||||||
isOpen={estimateFormOpen}
|
|
||||||
data={estimateToUpdate}
|
|
||||||
handleClose={() => {
|
|
||||||
setEstimateFormOpen(false);
|
|
||||||
setEstimateToUpdate(undefined);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteEstimateModal
|
|
||||||
isOpen={!!estimateToDelete}
|
|
||||||
handleClose={() => setEstimateToDelete(null)}
|
|
||||||
data={getProjectEstimateById(estimateToDelete!)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section className="flex items-center justify-between border-b border-custom-border-100 py-3.5">
|
|
||||||
<h3 className="text-xl font-medium">Estimates</h3>
|
|
||||||
<div className="col-span-12 space-y-5 sm:col-span-7">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setEstimateFormOpen(true);
|
|
||||||
setEstimateToUpdate(undefined);
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Add Estimate
|
|
||||||
</Button>
|
|
||||||
{currentProjectDetails?.estimate && (
|
|
||||||
<Button variant="neutral-primary" onClick={disableEstimates} size="sm">
|
|
||||||
Disable Estimates
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{projectEstimates ? (
|
|
||||||
projectEstimates.length > 0 ? (
|
|
||||||
<section className="h-full overflow-y-auto bg-custom-background-100">
|
|
||||||
{projectEstimates.map((estimate) => (
|
|
||||||
<EstimateListItem
|
|
||||||
key={estimate.id}
|
|
||||||
estimate={estimate}
|
|
||||||
editEstimate={(estimate) => editEstimate(estimate)}
|
|
||||||
deleteEstimate={(estimateId) => setEstimateToDelete(estimateId)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full py-8">
|
|
||||||
<EmptyState type={EmptyStateType.PROJECT_SETTINGS_ESTIMATE} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Loader className="mt-5 space-y-5">
|
|
||||||
<Loader.Item height="40px" />
|
|
||||||
<Loader.Item height="40px" />
|
|
||||||
<Loader.Item height="40px" />
|
|
||||||
<Loader.Item height="40px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,200 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { CalendarCheck2, CalendarClock } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { TBulkIssueProperties } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { DateDropdown, MemberDropdown, PriorityDropdown, StateDropdown } from "@/components/dropdowns";
|
|
||||||
import { IssueLabelSelect } from "@/components/issues/select";
|
|
||||||
import { CreateLabelModal } from "@/components/labels";
|
|
||||||
// constants
|
|
||||||
import { EErrorCodes, ERROR_DETAILS } from "@/constants/errors";
|
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
|
||||||
// helpers
|
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
|
||||||
// hooks
|
|
||||||
import { useIssues } from "@/hooks/store";
|
|
||||||
import { TSelectionHelper, TSelectionSnapshot } from "@/hooks/use-multiple-select";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectionHelpers: TSelectionHelper;
|
|
||||||
snapshot: TSelectionSnapshot;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: TBulkIssueProperties = {
|
|
||||||
state_id: "",
|
|
||||||
// @ts-expect-error priority should not be undefined, but it should be, in this case
|
|
||||||
priority: undefined,
|
|
||||||
assignee_ids: [],
|
|
||||||
start_date: null,
|
|
||||||
target_date: null,
|
|
||||||
label_ids: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssueBulkOperationsProperties: React.FC<Props> = (props) => {
|
|
||||||
const { snapshot } = props;
|
|
||||||
// states
|
|
||||||
const [createLabelModal, setCreateLabelModal] = useState(false);
|
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const {
|
|
||||||
issues: { bulkUpdateProperties },
|
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { dirtyFields, isDirty, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
watch,
|
|
||||||
} = useForm<TBulkIssueProperties>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleBulkOperations = async (data: TBulkIssueProperties) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
const payload: Partial<TBulkIssueProperties> = {};
|
|
||||||
Object.keys(dirtyFields).forEach((key) => {
|
|
||||||
const payloadKey = key as keyof typeof dirtyFields;
|
|
||||||
// @ts-expect-error values might not match
|
|
||||||
payload[payloadKey] = data[payloadKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
await bulkUpdateProperties(workspaceSlug.toString(), projectId.toString(), {
|
|
||||||
issue_ids: snapshot.selectedEntityIds,
|
|
||||||
properties: payload,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
reset(defaultValues);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const errorInfo = ERROR_DETAILS[error?.error_code as EErrorCodes] ?? undefined;
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: errorInfo?.title ?? "Error!",
|
|
||||||
message: errorInfo?.message ?? "Something went wrong. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const isUpdateDisabled = !snapshot.isSelectionActive;
|
|
||||||
|
|
||||||
const startDate = watch("start_date");
|
|
||||||
const targetDate = watch("target_date");
|
|
||||||
|
|
||||||
const minDate = getDate(startDate);
|
|
||||||
minDate?.setDate(minDate.getDate());
|
|
||||||
|
|
||||||
const maxDate = getDate(targetDate);
|
|
||||||
maxDate?.setDate(maxDate.getDate());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(handleBulkOperations)} className="size-full flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Controller
|
|
||||||
name="state_id"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<StateDropdown
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
projectId={projectId?.toString() ?? ""}
|
|
||||||
buttonVariant="border-with-text"
|
|
||||||
disabled={isUpdateDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="priority"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<PriorityDropdown
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonVariant="border-with-text"
|
|
||||||
buttonClassName="!text-custom-text-300"
|
|
||||||
disabled={isUpdateDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="assignee_ids"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<MemberDropdown
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
|
||||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
|
|
||||||
placeholder="Assignees"
|
|
||||||
multiple
|
|
||||||
disabled={isUpdateDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="start_date"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<DateDropdown
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => onChange(val ? renderFormattedPayloadDate(val) : null)}
|
|
||||||
buttonVariant="border-with-text"
|
|
||||||
placeholder="Start date"
|
|
||||||
icon={<CalendarClock className="size-3 flex-shrink-0" />}
|
|
||||||
disabled={isUpdateDisabled}
|
|
||||||
maxDate={maxDate ?? undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="target_date"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<DateDropdown
|
|
||||||
value={value}
|
|
||||||
onChange={(val) => onChange(val ? renderFormattedPayloadDate(val) : null)}
|
|
||||||
buttonVariant="border-with-text"
|
|
||||||
placeholder="Due date"
|
|
||||||
icon={<CalendarCheck2 className="size-3 flex-shrink-0" />}
|
|
||||||
disabled={isUpdateDisabled}
|
|
||||||
minDate={minDate ?? undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{projectId && (
|
|
||||||
<Controller
|
|
||||||
name="label_ids"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<>
|
|
||||||
<CreateLabelModal
|
|
||||||
isOpen={createLabelModal}
|
|
||||||
handleClose={() => setCreateLabelModal(false)}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
onSuccess={(res) => onChange([...value, res.id])}
|
|
||||||
/>
|
|
||||||
<IssueLabelSelect
|
|
||||||
value={value}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
onChange={onChange}
|
|
||||||
setIsOpen={() => setCreateLabelModal(true)}
|
|
||||||
buttonClassName="text-custom-text-300"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isDirty && (
|
|
||||||
<Button type="submit" variant="primary" size="sm" className="py-1" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating" : "Update"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC, ReactNode } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
// import Router from "next/navigation";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import NProgress from "nprogress";
|
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
// ui
|
|
||||||
import { Toast } from "@plane/ui";
|
|
||||||
// constants
|
|
||||||
import { SWR_CONFIG } from "@/constants/swr-config";
|
|
||||||
//helpers
|
|
||||||
import { resolveGeneralTheme } from "@/helpers/theme.helper";
|
|
||||||
// hooks
|
|
||||||
import { useInstance, useWorkspace, useUser } from "@/hooks/store";
|
|
||||||
// wrappers
|
|
||||||
import { InstanceWrapper } from "@/lib/wrappers";
|
|
||||||
// dynamic imports
|
|
||||||
const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false });
|
|
||||||
const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false });
|
|
||||||
const CrispWrapper = dynamic(() => import("@/lib/wrappers/crisp-wrapper"), { ssr: false });
|
|
||||||
// nprogress
|
|
||||||
NProgress.configure({ showSpinner: false });
|
|
||||||
// Router.events.on("routeChangeStart", NProgress.start);
|
|
||||||
// Router.events.on("routeChangeError", NProgress.done);
|
|
||||||
// Router.events.on("routeChangeComplete", NProgress.done);
|
|
||||||
|
|
||||||
export interface IAppProvider {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|
||||||
const { children } = props;
|
|
||||||
// store hooks
|
|
||||||
const { config } = useInstance();
|
|
||||||
const {
|
|
||||||
data: currentUser,
|
|
||||||
membership: { currentProjectRole, currentWorkspaceRole },
|
|
||||||
} = useUser();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
// themes
|
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<InstanceWrapper>
|
|
||||||
<StoreWrapper>
|
|
||||||
<CrispWrapper user={currentUser}>
|
|
||||||
<PostHogProvider
|
|
||||||
user={currentUser}
|
|
||||||
currentWorkspaceId={currentWorkspace?.id}
|
|
||||||
workspaceRole={currentWorkspaceRole}
|
|
||||||
projectRole={currentProjectRole}
|
|
||||||
posthogAPIKey={config?.posthog_api_key || undefined}
|
|
||||||
posthogHost={config?.posthog_host || undefined}
|
|
||||||
>
|
|
||||||
{/* TODO: Need to handle custom themes for toast */}
|
|
||||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
|
||||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
|
||||||
</PostHogProvider>
|
|
||||||
</CrispWrapper>
|
|
||||||
</StoreWrapper>
|
|
||||||
</InstanceWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user