forked from github/plane
236 lines
7.9 KiB
TypeScript
236 lines
7.9 KiB
TypeScript
import { useState } from "react";
|
|
import { add } from "date-fns";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { Calendar } from "lucide-react";
|
|
import { IApiToken } from "@plane/types";
|
|
// ui
|
|
import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
|
import { DateDropdown } from "@/components/dropdowns";
|
|
// helpers
|
|
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
|
// types
|
|
|
|
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)))
|
|
return setToast({
|
|
type: TOAST_TYPE.ERROR,
|
|
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
|
|
else if (data.expired_at === "custom") payload.expired_at = renderFormattedPayloadDate(customDate);
|
|
// 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 ?? "");
|
|
|
|
if (expiryDate) payload.expired_at = renderFormattedPayloadDate(new Date(expiryDate));
|
|
}
|
|
|
|
await onSubmit(payload).then(() => {
|
|
reset(defaultValues);
|
|
setCustomDate(null);
|
|
});
|
|
};
|
|
|
|
const today = new Date();
|
|
const tomorrow = add(today, { days: 1 });
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Create token</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<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)}
|
|
placeholder="Token title"
|
|
className="w-full text-sm font-medium"
|
|
/>
|
|
)}
|
|
/>
|
|
{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)}
|
|
placeholder="Token description"
|
|
className="min-h-24 w-full text-sm"
|
|
/>
|
|
)}
|
|
/>
|
|
<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
|
|
className={`flex items-center gap-2 rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 ${
|
|
neverExpires ? "text-custom-text-400" : ""
|
|
}`}
|
|
>
|
|
<Calendar className="h-3 w-3" />
|
|
{value === "custom"
|
|
? "Custom date"
|
|
: selectedOption
|
|
? selectedOption.label
|
|
: "Set expiration date"}
|
|
</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>
|
|
);
|
|
}}
|
|
/>
|
|
{watch("expired_at") === "custom" && (
|
|
<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>
|
|
{!neverExpires && (
|
|
<span className="text-xs text-custom-text-400">
|
|
{watch("expired_at") === "custom"
|
|
? customDate
|
|
? `Expires ${renderFormattedDate(customDate)}`
|
|
: null
|
|
: watch("expired_at")
|
|
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}`
|
|
: null}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 flex items-center justify-between gap-2">
|
|
<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}>
|
|
Discard
|
|
</Button>
|
|
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
|
{isSubmitting ? "Generating..." : "Generate token"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|