2024-06-10 08:06:10 +00:00
|
|
|
"use client";
|
|
|
|
|
2023-11-27 06:44:06 +00:00
|
|
|
import { useState } from "react";
|
|
|
|
import { add } from "date-fns";
|
|
|
|
import { Controller, useForm } from "react-hook-form";
|
|
|
|
import { Calendar } from "lucide-react";
|
2024-05-07 07:14:36 +00:00
|
|
|
// types
|
2024-03-19 14:38:35 +00:00
|
|
|
import { IApiToken } from "@plane/types";
|
2023-11-27 06:44:06 +00:00
|
|
|
// ui
|
2024-03-06 08:48:41 +00:00
|
|
|
import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
2024-05-07 07:14:36 +00:00
|
|
|
// components
|
2024-03-19 14:38:35 +00:00
|
|
|
import { DateDropdown } from "@/components/dropdowns";
|
2023-11-27 06:44:06 +00:00
|
|
|
// helpers
|
2024-05-07 07:14:36 +00:00
|
|
|
import { cn } from "@/helpers/common.helper";
|
2024-03-19 14:38:35 +00:00
|
|
|
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
2023-11-27 06:44:06 +00:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
handleClose: () => void;
|
|
|
|
neverExpires: boolean;
|
|
|
|
toggleNeverExpires: () => void;
|
|
|
|
onSubmit: (data: Partial<IApiToken>) => Promise<void>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const EXPIRY_DATE_OPTIONS = [
|
|
|
|
{
|
|
|
|
key: "1_week",
|
|
|
|
label: "1 week",
|
|
|
|
value: { weeks: 1 },
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "1_month",
|
|
|
|
label: "1 month",
|
|
|
|
value: { months: 1 },
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "3_months",
|
|
|
|
label: "3 months",
|
|
|
|
value: { months: 3 },
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "1_year",
|
|
|
|
label: "1 year",
|
|
|
|
value: { years: 1 },
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
const defaultValues: Partial<IApiToken> = {
|
|
|
|
label: "",
|
|
|
|
description: "",
|
|
|
|
expired_at: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
const getExpiryDate = (val: string): string | null => {
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
const dateToAdd = EXPIRY_DATE_OPTIONS.find((option) => option.key === val)?.value;
|
|
|
|
|
|
|
|
if (dateToAdd) {
|
|
|
|
const expiryDate = add(today, dateToAdd);
|
|
|
|
|
|
|
|
return renderFormattedDate(expiryDate);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const CreateApiTokenForm: React.FC<Props> = (props) => {
|
|
|
|
const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props;
|
|
|
|
// states
|
|
|
|
const [customDate, setCustomDate] = useState<Date | null>(null);
|
|
|
|
// form
|
|
|
|
const {
|
|
|
|
control,
|
|
|
|
formState: { errors, isSubmitting },
|
|
|
|
handleSubmit,
|
|
|
|
reset,
|
|
|
|
watch,
|
|
|
|
} = useForm<IApiToken>({ defaultValues });
|
|
|
|
|
|
|
|
const handleFormSubmit = async (data: IApiToken) => {
|
|
|
|
// if never expires is toggled off, and the user has not selected a custom date or a predefined date, show an error
|
|
|
|
if (!neverExpires && (!data.expired_at || (data.expired_at === "custom" && !customDate)))
|
2024-03-06 08:48:41 +00:00
|
|
|
return setToast({
|
|
|
|
type: TOAST_TYPE.ERROR,
|
2023-11-27 06:44:06 +00:00
|
|
|
title: "Error!",
|
|
|
|
message: "Please select an expiration date.",
|
|
|
|
});
|
|
|
|
|
|
|
|
const payload: Partial<IApiToken> = {
|
|
|
|
label: data.label,
|
|
|
|
description: data.description,
|
|
|
|
};
|
|
|
|
|
|
|
|
// if never expires is toggled on, set expired_at to null
|
|
|
|
if (neverExpires) payload.expired_at = null;
|
|
|
|
// if never expires is toggled off, and the user has selected a custom date, set expired_at to the custom date
|
2024-03-20 08:14:08 +00:00
|
|
|
else if (data.expired_at === "custom") payload.expired_at = renderFormattedPayloadDate(customDate);
|
2023-11-27 06:44:06 +00:00
|
|
|
// if never expires is toggled off, and the user has selected a predefined date, set expired_at to the predefined date
|
|
|
|
else {
|
|
|
|
const expiryDate = getExpiryDate(data.expired_at ?? "");
|
|
|
|
|
2024-04-10 15:57:22 +00:00
|
|
|
if (expiryDate) payload.expired_at = renderFormattedPayloadDate(new Date(expiryDate));
|
2023-11-27 06:44:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await onSubmit(payload).then(() => {
|
|
|
|
reset(defaultValues);
|
|
|
|
setCustomDate(null);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const today = new Date();
|
|
|
|
const tomorrow = add(today, { days: 1 });
|
2024-05-07 07:14:36 +00:00
|
|
|
const expiredAt = watch("expired_at");
|
2023-11-27 06:44:06 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
2024-05-07 07:14:36 +00:00
|
|
|
<div className="space-y-5 p-5">
|
|
|
|
<h3 className="text-xl font-medium text-custom-text-200">Create token</h3>
|
2023-11-27 06:44:06 +00:00
|
|
|
<div className="space-y-3">
|
2024-05-07 07:14:36 +00:00
|
|
|
<div className="space-y-1">
|
2023-11-27 06:44:06 +00:00
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
name="label"
|
|
|
|
rules={{
|
|
|
|
required: "Title is required",
|
|
|
|
maxLength: {
|
|
|
|
value: 255,
|
|
|
|
message: "Title should be less than 255 characters",
|
|
|
|
},
|
|
|
|
validate: (val) => val.trim() !== "" || "Title is required",
|
|
|
|
}}
|
|
|
|
render={({ field: { value, onChange } }) => (
|
|
|
|
<Input
|
|
|
|
type="text"
|
|
|
|
value={value}
|
|
|
|
onChange={onChange}
|
|
|
|
hasError={Boolean(errors.label)}
|
2024-05-07 07:14:36 +00:00
|
|
|
placeholder="Title"
|
|
|
|
className="w-full text-base"
|
2023-11-27 06:44:06 +00:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
{errors.label && <span className="text-xs text-red-500">{errors.label.message}</span>}
|
|
|
|
</div>
|
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
name="description"
|
|
|
|
render={({ field: { value, onChange } }) => (
|
|
|
|
<TextArea
|
|
|
|
value={value}
|
|
|
|
onChange={onChange}
|
|
|
|
hasError={Boolean(errors.description)}
|
2024-05-07 07:14:36 +00:00
|
|
|
placeholder="Description"
|
|
|
|
className="w-full text-base resize-none min-h-24"
|
2023-11-27 06:44:06 +00:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
name="expired_at"
|
|
|
|
render={({ field: { onChange, value } }) => {
|
|
|
|
const selectedOption = EXPIRY_DATE_OPTIONS.find((option) => option.key === value);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<CustomSelect
|
|
|
|
customButton={
|
|
|
|
<div
|
2024-05-07 07:14:36 +00:00
|
|
|
className={cn(
|
|
|
|
"h-7 flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5",
|
|
|
|
{
|
|
|
|
"text-custom-text-400": neverExpires,
|
|
|
|
}
|
|
|
|
)}
|
2023-11-27 06:44:06 +00:00
|
|
|
>
|
|
|
|
<Calendar className="h-3 w-3" />
|
|
|
|
{value === "custom"
|
|
|
|
? "Custom date"
|
|
|
|
: selectedOption
|
2024-04-29 13:28:31 +00:00
|
|
|
? selectedOption.label
|
|
|
|
: "Set expiration date"}
|
2023-11-27 06:44:06 +00:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
value={value}
|
|
|
|
onChange={onChange}
|
|
|
|
disabled={neverExpires}
|
|
|
|
>
|
|
|
|
{EXPIRY_DATE_OPTIONS.map((option) => (
|
|
|
|
<CustomSelect.Option key={option.key} value={option.key}>
|
|
|
|
{option.label}
|
|
|
|
</CustomSelect.Option>
|
|
|
|
))}
|
|
|
|
<CustomSelect.Option value="custom">Custom</CustomSelect.Option>
|
|
|
|
</CustomSelect>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
/>
|
2024-05-07 07:14:36 +00:00
|
|
|
{expiredAt === "custom" && (
|
|
|
|
<div className="h-7">
|
|
|
|
<DateDropdown
|
|
|
|
value={customDate}
|
|
|
|
onChange={(date) => setCustomDate(date)}
|
|
|
|
minDate={tomorrow}
|
|
|
|
icon={<Calendar className="h-3 w-3" />}
|
|
|
|
buttonVariant="border-with-text"
|
|
|
|
placeholder="Set date"
|
|
|
|
disabled={neverExpires}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-11-27 06:44:06 +00:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
{!neverExpires && (
|
|
|
|
<span className="text-xs text-custom-text-400">
|
2024-05-07 07:14:36 +00:00
|
|
|
{expiredAt === "custom"
|
2023-11-27 06:44:06 +00:00
|
|
|
? customDate
|
|
|
|
? `Expires ${renderFormattedDate(customDate)}`
|
|
|
|
: null
|
2024-05-07 07:14:36 +00:00
|
|
|
: expiredAt
|
|
|
|
? `Expires ${getExpiryDate(expiredAt ?? "")}`
|
2024-04-29 13:28:31 +00:00
|
|
|
: null}
|
2023-11-27 06:44:06 +00:00
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-05-07 07:14:36 +00:00
|
|
|
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
2023-11-27 06:44:06 +00:00
|
|
|
<div className="flex cursor-pointer items-center gap-1.5" onClick={toggleNeverExpires}>
|
|
|
|
<div className="flex cursor-pointer items-center justify-center">
|
|
|
|
<ToggleSwitch value={neverExpires} onChange={() => {}} size="sm" />
|
|
|
|
</div>
|
|
|
|
<span className="text-xs">Never expires</span>
|
|
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
2024-05-07 07:14:36 +00:00
|
|
|
Cancel
|
2023-11-27 06:44:06 +00:00
|
|
|
</Button>
|
|
|
|
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
2024-05-07 07:14:36 +00:00
|
|
|
{isSubmitting ? "Generating" : "Generate token"}
|
2023-11-27 06:44:06 +00:00
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
};
|