mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 02:29:26 +00:00
Merge pull request #3926 from Infisical/feat/remindersImprovement
feat(secret-reminders): rework secret reminders logic
This commit is contained in:
@@ -20,6 +20,7 @@ export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
popUpProps: PopoverProps;
|
||||
popUpContentProps: PopoverContentProps;
|
||||
dateFormat?: "PPP" | "PP" | "P"; // extend as needed
|
||||
hideTime?: boolean;
|
||||
buttonClassName?: string;
|
||||
timezone?: Timezone;
|
||||
};
|
||||
@@ -55,6 +56,7 @@ export const DatePicker = ({
|
||||
popUpProps,
|
||||
popUpContentProps,
|
||||
dateFormat = "PPP",
|
||||
hideTime = false,
|
||||
buttonClassName,
|
||||
timezone,
|
||||
...props
|
||||
@@ -124,14 +126,16 @@ export const DatePicker = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
value={displayUtc ? localTimeToUTC(timeValue) : timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
{!hideTime && (
|
||||
<div className="mx-4 my-4">
|
||||
<Input
|
||||
type="time"
|
||||
value={displayUtc ? localTimeToUTC(timeValue) : timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="bg-mineshaft-700 text-white [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { useCreateReminder, useDeleteReminder, useGetReminder } from "./queries";
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { CreateReminderDTO, DeleteReminderDTO, Reminder } from "./types";
|
||||
|
||||
export const reminderKeys = {
|
||||
getReminder: (secretId: string) => ["get-reminder", secretId] as const
|
||||
};
|
||||
|
||||
export const useCreateReminder = (secretId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Reminder, object, CreateReminderDTO>({
|
||||
mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => {
|
||||
const { data } = await apiRequest.post<{ reminder: Reminder }>(
|
||||
`/api/v1/reminders/secrets/${secretId}`,
|
||||
{
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate,
|
||||
recipients
|
||||
}
|
||||
);
|
||||
return data.reminder;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteReminder = (secretId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Reminder, object, DeleteReminderDTO>({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiRequest.delete<{ reminder: Reminder }>(
|
||||
`/api/v1/reminders/secrets/${secretId}`
|
||||
);
|
||||
return data.reminder;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: reminderKeys.getReminder(secretId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetReminder = (secretId: string) => {
|
||||
return useQuery({
|
||||
queryKey: reminderKeys.getReminder(secretId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ reminder: Reminder }>(
|
||||
`/api/v1/reminders/secrets/${secretId}`
|
||||
);
|
||||
return data.reminder;
|
||||
},
|
||||
enabled: Boolean(secretId)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export type CreateReminderDTO = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: Date | null;
|
||||
secretId: string;
|
||||
recipients?: string[];
|
||||
};
|
||||
|
||||
export type DeleteReminderDTO = {
|
||||
secretId: string;
|
||||
reminderId: string;
|
||||
};
|
||||
|
||||
export type Reminder = { id: string } & CreateReminderDTO;
|
||||
+388
-126
@@ -1,25 +1,65 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { useCreateReminder, useDeleteReminder } from "@app/hooks/api/reminders";
|
||||
import { reminderKeys } from "@app/hooks/api/reminders/queries";
|
||||
import { Reminder } from "@app/hooks/api/reminders/types";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
|
||||
// Constants
|
||||
const MIN_REPEAT_DAYS = 1;
|
||||
const MAX_REPEAT_DAYS = 365;
|
||||
const DEFAULT_REPEAT_DAYS = 30;
|
||||
const DEFAULT_TEXTAREA_ROWS = 8;
|
||||
|
||||
// Enums
|
||||
enum ReminderType {
|
||||
Recurring = "Recurring",
|
||||
OneTime = "One Time"
|
||||
}
|
||||
|
||||
// Types
|
||||
interface RecipientOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ReminderFormProps {
|
||||
isOpen: boolean;
|
||||
reminderId?: string;
|
||||
onOpenChange: () => void;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
reminder?: Reminder;
|
||||
}
|
||||
|
||||
// Validation Schema
|
||||
const ReminderFormSchema = z.object({
|
||||
note: z.string().optional().nullable(),
|
||||
message: z.string().optional().nullable(),
|
||||
recipients: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -28,182 +68,404 @@ const ReminderFormSchema = z.object({
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
days: z
|
||||
repeatDays: z
|
||||
.number()
|
||||
.min(1, { message: "Must be at least 1 day" })
|
||||
.max(365, { message: "Must be less than 365 days" })
|
||||
.min(MIN_REPEAT_DAYS, { message: `Must be at least ${MIN_REPEAT_DAYS} day` })
|
||||
.max(MAX_REPEAT_DAYS, { message: `Must be less than ${MAX_REPEAT_DAYS} days` })
|
||||
.nullable()
|
||||
.optional(),
|
||||
nextReminderDate: z.coerce
|
||||
.date()
|
||||
.refine((data) => data > new Date(), { message: "Reminder date must be in the future" })
|
||||
.nullable()
|
||||
.optional(),
|
||||
reminderType: z.enum(["Recurring", "One Time"])
|
||||
});
|
||||
|
||||
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||
|
||||
interface ReminderFormProps {
|
||||
isOpen: boolean;
|
||||
repeatDays?: number | null;
|
||||
note?: string | null;
|
||||
recipients?: string[] | null;
|
||||
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
|
||||
}
|
||||
// Custom hook for form state management
|
||||
const useReminderForm = (reminderData?: Reminder) => {
|
||||
const { repeatDays, message, nextReminderDate } = reminderData || {};
|
||||
|
||||
const isEditMode = Boolean(reminderData);
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
repeatDays: repeatDays || null,
|
||||
message: message || "",
|
||||
nextReminderDate: nextReminderDate || null,
|
||||
reminderType: repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||
recipients: []
|
||||
}),
|
||||
[repeatDays, message, nextReminderDate]
|
||||
);
|
||||
|
||||
return {
|
||||
isEditMode,
|
||||
reminderData,
|
||||
defaultValues
|
||||
};
|
||||
};
|
||||
|
||||
// Custom hook for workspace members
|
||||
const useWorkspaceMembers = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id);
|
||||
|
||||
const memberOptions = useMemo(
|
||||
(): RecipientOption[] =>
|
||||
members.map((member) => ({
|
||||
label: member.user.username || member.user.email,
|
||||
value: member.user.id
|
||||
})),
|
||||
[members]
|
||||
);
|
||||
|
||||
return { members, memberOptions };
|
||||
};
|
||||
|
||||
// Main component
|
||||
export const CreateReminderForm = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
repeatDays,
|
||||
note,
|
||||
recipients
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretId,
|
||||
reminder
|
||||
}: ReminderFormProps) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const queryClient = useQueryClient();
|
||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||
|
||||
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id);
|
||||
// Custom hooks
|
||||
const { isEditMode, reminderData } = useReminderForm(reminder);
|
||||
const { memberOptions } = useWorkspaceMembers();
|
||||
|
||||
// API mutations
|
||||
const { mutateAsync: createReminder } = useCreateReminder(secretId);
|
||||
const { mutateAsync: deleteReminder } = useDeleteReminder(secretId);
|
||||
|
||||
// Form setup
|
||||
const form = useForm<TReminderFormSchema>({
|
||||
defaultValues: {
|
||||
repeatDays: reminderData?.repeatDays || null,
|
||||
message: reminderData?.message || "",
|
||||
nextReminderDate: reminderData?.nextReminderDate || null,
|
||||
reminderType: reminderData?.repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||
recipients: []
|
||||
},
|
||||
resolver: zodResolver(ReminderFormSchema)
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TReminderFormSchema>({
|
||||
defaultValues: {
|
||||
days: repeatDays || undefined,
|
||||
note: note || ""
|
||||
},
|
||||
resolver: zodResolver(ReminderFormSchema)
|
||||
});
|
||||
} = form;
|
||||
|
||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||
onOpenChange(false, data);
|
||||
// Watch form values
|
||||
const reminderType = watch("reminderType");
|
||||
|
||||
// Invalidate queries helper
|
||||
const invalidateQueries = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({
|
||||
projectId: workspaceId,
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: reminderKeys.getReminder(secretId)
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// On initial load, filter the members to only include the recipients
|
||||
if (members.length) {
|
||||
const filteredMembers = members.filter((m) => recipients?.find((r) => r === m.user.id));
|
||||
setValue(
|
||||
"recipients",
|
||||
filteredMembers.map((m) => ({
|
||||
label: m.user.username || m.user.email,
|
||||
value: m.user.id
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [members, isOpen, recipients]);
|
||||
// Form submission handler
|
||||
const handleFormSubmit = async (data: TReminderFormSchema) => {
|
||||
try {
|
||||
await createReminder({
|
||||
repeatDays: data.repeatDays,
|
||||
message: data.message,
|
||||
recipients: data.recipients?.map((r) => r.value) || [],
|
||||
secretId,
|
||||
nextReminderDate: data.nextReminderDate
|
||||
});
|
||||
|
||||
invalidateQueries();
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: `Successfully ${isEditMode ? "updated" : "created"} secret reminder`
|
||||
});
|
||||
|
||||
reset();
|
||||
onOpenChange();
|
||||
} catch (error) {
|
||||
console.error("Failed to save reminder:", error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to save reminder. Please try again."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delete reminder handler
|
||||
const handleDeleteReminder = async () => {
|
||||
try {
|
||||
await deleteReminder({ reminderId: reminder?.id || "", secretId });
|
||||
invalidateQueries();
|
||||
reset();
|
||||
onOpenChange();
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted reminder"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete reminder:", error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete reminder. Please try again."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle reminder type change
|
||||
const handleReminderTypeChange = useCallback(
|
||||
(newType: string) => {
|
||||
if (newType === ReminderType.Recurring) {
|
||||
setValue("repeatDays", DEFAULT_REPEAT_DAYS);
|
||||
setValue("nextReminderDate", null);
|
||||
} else if (newType === ReminderType.OneTime) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
setValue("nextReminderDate", tomorrow);
|
||||
setValue("repeatDays", null);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
// Initialize form with existing data
|
||||
useEffect(() => {
|
||||
if (repeatDays) setValue("days", repeatDays);
|
||||
if (note) setValue("note", note);
|
||||
}, [repeatDays, note]);
|
||||
if (!reminderData) return;
|
||||
|
||||
const {
|
||||
repeatDays: repeatDaysInitial,
|
||||
message,
|
||||
recipients,
|
||||
nextReminderDate: nextReminderDateInitial
|
||||
} = reminderData;
|
||||
|
||||
if (repeatDaysInitial) {
|
||||
setValue("repeatDays", repeatDaysInitial);
|
||||
setValue("reminderType", ReminderType.Recurring);
|
||||
} else {
|
||||
setValue("reminderType", ReminderType.OneTime);
|
||||
}
|
||||
|
||||
if (message) setValue("message", message);
|
||||
if (nextReminderDateInitial) setValue("nextReminderDate", nextReminderDateInitial);
|
||||
|
||||
// Set recipients
|
||||
if (recipients?.length && memberOptions.length) {
|
||||
const selectedRecipients = memberOptions.filter((option) =>
|
||||
recipients.includes(option.value)
|
||||
);
|
||||
setValue("recipients", selectedRecipients);
|
||||
}
|
||||
}, [reminderData, memberOptions, setValue]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title={`${repeatDays ? "Update" : "Create"} reminder`}
|
||||
title={`${isEditMode ? "Update" : "Create"} reminder`}
|
||||
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="days"
|
||||
render={({ field, fieldState }) => (
|
||||
<>
|
||||
<FormControl
|
||||
isRequired
|
||||
className="mb-0"
|
||||
label="Reminder Interval (in days)"
|
||||
isError={Boolean(fieldState.error)}
|
||||
errorText={fieldState.error?.message || ""}
|
||||
>
|
||||
<Input
|
||||
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
|
||||
type="number"
|
||||
placeholder="31"
|
||||
defaultValue={repeatDays || undefined}
|
||||
value={field.value || undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
<div
|
||||
className={twMerge(
|
||||
"ml-1 mt-2 text-xs",
|
||||
field.value ? "opacity-60" : "opacity-0"
|
||||
)}
|
||||
>
|
||||
A reminder will be sent every{" "}
|
||||
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormControl label="Note" className="mb-0">
|
||||
<TextArea
|
||||
placeholder="Remember to rotate the AWS secret every month."
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={8}
|
||||
defaultValue={note || ""}
|
||||
reSize="none"
|
||||
cols={30}
|
||||
{...register("note")}
|
||||
/>
|
||||
</FormControl>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||
{/* Reminder Type Selection */}
|
||||
<Controller
|
||||
name="reminderType"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Recurrence"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
handleReminderTypeChange(val);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select reminder recurrence"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ReminderType).map((type) => (
|
||||
<SelectItem value={type} key={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Conditional Fields Based on Reminder Type */}
|
||||
{reminderType === ReminderType.Recurring ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name="recipients"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
tooltipText={
|
||||
<div>
|
||||
Select users to receive reminders.
|
||||
<br />
|
||||
<br /> If none are selected, all project members will receive the reminder.
|
||||
</div>
|
||||
}
|
||||
label="Recipients"
|
||||
className="mb-0"
|
||||
>
|
||||
<FilterableSelect
|
||||
className="w-full"
|
||||
placeholder="Select reminder recipients..."
|
||||
isMulti
|
||||
name="recipients"
|
||||
options={members.map((member) => ({
|
||||
label: member.user.username || member.user.email,
|
||||
value: member.user.id
|
||||
}))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
name="repeatDays"
|
||||
render={({ field, fieldState }) => (
|
||||
<div>
|
||||
<FormControl
|
||||
isRequired
|
||||
className="mb-0"
|
||||
label="Reminder Interval (in days)"
|
||||
isError={Boolean(fieldState.error)}
|
||||
errorText={fieldState.error?.message || ""}
|
||||
>
|
||||
<Input
|
||||
onChange={(el) => {
|
||||
const value = parseInt(el.target.value, 10);
|
||||
setValue("repeatDays", Number.isNaN(value) ? null : value);
|
||||
}}
|
||||
type="number"
|
||||
placeholder={DEFAULT_REPEAT_DAYS.toString()}
|
||||
value={field.value || ""}
|
||||
min={MIN_REPEAT_DAYS}
|
||||
max={MAX_REPEAT_DAYS}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* Interval description */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"ml-1 mt-2 text-xs",
|
||||
field.value ? "opacity-60" : "opacity-0"
|
||||
)}
|
||||
>
|
||||
A reminder will be sent every{" "}
|
||||
{field.value && field.value > 1 ? `${field.value} days` : "day"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-7 flex items-center space-x-4">
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name="nextReminderDate"
|
||||
render={({ field, fieldState }) => (
|
||||
<div>
|
||||
<FormControl
|
||||
isRequired
|
||||
className="mb-0"
|
||||
label="Reminder Date"
|
||||
isError={Boolean(fieldState.error)}
|
||||
errorText={fieldState.error?.message || ""}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
className="w-full"
|
||||
onChange={field.onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isDatePickerOpen,
|
||||
onOpenChange: setIsDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
hideTime
|
||||
hidden={{ before: new Date(Date.now() + 86400000) }}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Message/Note Field */}
|
||||
<FormControl label="Note" className="mb-0">
|
||||
<TextArea
|
||||
placeholder="Remember to rotate the AWS secret every month."
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={DEFAULT_TEXTAREA_ROWS}
|
||||
reSize="none"
|
||||
cols={30}
|
||||
{...register("message")}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* Recipients Selection */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="recipients"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
tooltipText={
|
||||
<div>
|
||||
Select users to receive reminders.
|
||||
<br />
|
||||
<br /> If none are selected, all project members will receive the reminder.
|
||||
</div>
|
||||
}
|
||||
label="Recipients"
|
||||
className="mb-0"
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
className="w-full"
|
||||
placeholder="Select reminder recipients..."
|
||||
isMulti
|
||||
name="recipients"
|
||||
options={memberOptions}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center space-x-4 pt-4">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className=""
|
||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||
type="submit"
|
||||
>
|
||||
{repeatDays ? "Update" : "Create"} reminder
|
||||
{isEditMode ? "Update" : "Create"} reminder
|
||||
</Button>
|
||||
{repeatDays && (
|
||||
|
||||
{isEditMode && (
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => onOpenChange(false, { days: null, note: null })}
|
||||
onClick={handleDeleteReminder}
|
||||
colorSchema="danger"
|
||||
variant="outline"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
type="button"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Delete reminder
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={onOpenChange}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
type="button"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
+26
-36
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
@@ -56,6 +56,7 @@ import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { useGetReminder } from "@app/hooks/api/reminders";
|
||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -115,6 +116,7 @@ export const SecretDetailSidebar = ({
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: reminderData } = useGetReminder(secret?.id);
|
||||
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
@@ -221,23 +223,8 @@ export const SecretDetailSidebar = ({
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
};
|
||||
|
||||
const handleReminderSubmit = async (
|
||||
reminderRepeatDays: number | null | undefined,
|
||||
reminderNote: string | null | undefined,
|
||||
reminderRecipients: string[] | undefined
|
||||
) => {
|
||||
await onSaveSecret(
|
||||
secret,
|
||||
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true, reminderRecipients },
|
||||
() => {}
|
||||
);
|
||||
};
|
||||
|
||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||
|
||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||
const secretReminderNote = watch("reminderNote");
|
||||
const secretReminderRecipients = watch("reminderRecipients");
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
"reminderRecipients",
|
||||
@@ -303,27 +290,33 @@ export const SecretDetailSidebar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getDaysUntilReminder = useMemo(() => {
|
||||
return (): string => {
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
const target = new Date(reminderData?.nextReminderDate || "");
|
||||
target.setHours(0, 0, 0, 0);
|
||||
|
||||
const diffTime = target.getTime() - now.getTime();
|
||||
const daysRemaining = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return `Days until next reminder: ${daysRemaining}`;
|
||||
};
|
||||
}, [reminderData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateReminderForm
|
||||
repeatDays={secretReminderRepeatDays}
|
||||
note={secretReminderNote}
|
||||
recipients={secretReminderRecipients}
|
||||
isOpen={createReminderFormOpen}
|
||||
onOpenChange={(_, data) => {
|
||||
onOpenChange={() => {
|
||||
setCreateReminderFormOpen.toggle();
|
||||
|
||||
if (data) {
|
||||
const recipients = data.recipients?.length
|
||||
? data.recipients.map((recipient) => recipient.value)
|
||||
: undefined;
|
||||
|
||||
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
|
||||
setValue("reminderNote", data.note, { shouldDirty: false });
|
||||
setValue("reminderRecipients", recipients, { shouldDirty: false });
|
||||
handleReminderSubmit(data.days, data.note, recipients);
|
||||
}
|
||||
}}
|
||||
workspaceId={currentWorkspace.id}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
secretId={secret?.id}
|
||||
reminder={reminderData}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.secretAccessUpgradePlan.isOpen}
|
||||
@@ -735,14 +728,11 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
/>
|
||||
<FormControl>
|
||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||
{reminderData && reminderData.nextReminderDate ? (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||
<span className="text-sm text-bunker-300">
|
||||
Reminder every {secretReminderRepeatDays}{" "}
|
||||
{secretReminderRepeatDays > 1 ? "days" : "day"}
|
||||
</span>
|
||||
<span className="text-sm text-bunker-300">{getDaysUntilReminder()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user