mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: Secret reminder from date filter (#4289)
* feat: add fromDate in reminders * feat: update reminder form * fix: lint * chore: generate schema * fix: reminder logic * fix: update ui * fix: pr change --------- Co-authored-by: sidwebworks <xodeveloper@gmail.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Reminder, "fromDate"))) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.timestamp("fromDate", { useTz: true }).nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) {
|
||||
await knex.schema.alterTable(TableName.Reminder, (t) => {
|
||||
t.dropColumn("fromDate");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ export const RemindersSchema = z.object({
|
||||
repeatDays: z.number().nullable().optional(),
|
||||
nextReminderDate: z.date(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
fromDate: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TReminders = z.infer<typeof RemindersSchema>;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
|
||||
message: z.string().trim().max(1024).optional(),
|
||||
repeatDays: z.number().min(1).nullable().optional(),
|
||||
nextReminderDate: z.string().datetime().nullable().optional(),
|
||||
fromDate: z.string().datetime().nullable().optional(),
|
||||
recipients: z.string().array().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
@@ -45,6 +46,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
|
||||
message: req.body.message,
|
||||
repeatDays: req.body.repeatDays,
|
||||
nextReminderDate: req.body.nextReminderDate,
|
||||
fromDate: req.body.fromDate,
|
||||
recipients: req.body.recipients
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,25 +79,33 @@ export const reminderServiceFactory = ({
|
||||
repeatDays,
|
||||
nextReminderDate: nextReminderDateInput,
|
||||
recipients,
|
||||
projectId
|
||||
projectId,
|
||||
fromDate: fromDateInput
|
||||
}: {
|
||||
secretId?: string;
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
fromDate?: string | null;
|
||||
projectId: string;
|
||||
}) => {
|
||||
if (!secretId) {
|
||||
throw new BadRequestError({ message: "secretId is required" });
|
||||
}
|
||||
let nextReminderDate;
|
||||
let fromDate;
|
||||
if (nextReminderDateInput) {
|
||||
nextReminderDate = new Date(nextReminderDateInput);
|
||||
}
|
||||
|
||||
if (repeatDays && repeatDays > 0) {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
if (repeatDays) {
|
||||
if (fromDateInput) {
|
||||
fromDate = new Date(fromDateInput);
|
||||
nextReminderDate = fromDate;
|
||||
} else {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextReminderDate) {
|
||||
@@ -112,7 +120,8 @@ export const reminderServiceFactory = ({
|
||||
await reminderDAL.updateById(existingReminder.id, {
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
});
|
||||
reminderId = existingReminder.id;
|
||||
} else {
|
||||
@@ -121,7 +130,8 @@ export const reminderServiceFactory = ({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
});
|
||||
reminderId = newReminder.id;
|
||||
}
|
||||
@@ -280,14 +290,28 @@ export const reminderServiceFactory = ({
|
||||
}
|
||||
|
||||
const processedReminders = remindersData.map(
|
||||
({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => {
|
||||
({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate: nextReminderDateInput,
|
||||
recipients,
|
||||
projectId,
|
||||
fromDate: fromDateInput
|
||||
}) => {
|
||||
let nextReminderDate;
|
||||
let fromDate;
|
||||
if (nextReminderDateInput) {
|
||||
nextReminderDate = new Date(nextReminderDateInput);
|
||||
}
|
||||
|
||||
if (repeatDays && repeatDays > 0 && !nextReminderDate) {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
if (repeatDays && !nextReminderDate) {
|
||||
if (fromDateInput) {
|
||||
fromDate = new Date(fromDateInput);
|
||||
nextReminderDate = fromDate;
|
||||
} else {
|
||||
nextReminderDate = $addDays(repeatDays);
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextReminderDate) {
|
||||
@@ -302,17 +326,19 @@ export const reminderServiceFactory = ({
|
||||
repeatDays,
|
||||
nextReminderDate,
|
||||
recipients: recipients ? [...new Set(recipients)] : [],
|
||||
projectId
|
||||
projectId,
|
||||
fromDate
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const newReminders = await reminderDAL.insertMany(
|
||||
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({
|
||||
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, fromDate }) => ({
|
||||
secretId,
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate
|
||||
nextReminderDate,
|
||||
fromDate
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TReminder = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate: Date;
|
||||
fromDate?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -21,6 +22,7 @@ export type TCreateReminderDTO = {
|
||||
secretId?: string;
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
fromDate?: string | null;
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
};
|
||||
@@ -31,6 +33,7 @@ export type TBatchCreateReminderDTO = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: string | Date | null;
|
||||
fromDate?: Date | null;
|
||||
recipients?: string[] | null;
|
||||
projectId?: string;
|
||||
}[];
|
||||
@@ -95,6 +98,7 @@ export interface TReminderServiceFactory {
|
||||
nextReminderDate?: string | null;
|
||||
recipients?: string[] | null;
|
||||
projectId: string;
|
||||
fromDate?: string | null;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
created: boolean;
|
||||
|
||||
@@ -12,14 +12,15 @@ export const useCreateReminder = (secretId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Reminder, object, CreateReminderDTO>({
|
||||
mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => {
|
||||
mutationFn: async ({ message, repeatDays, nextReminderDate, recipients, fromDate }) => {
|
||||
const { data } = await apiRequest.post<{ reminder: Reminder }>(
|
||||
`/api/v1/reminders/secrets/${secretId}`,
|
||||
{
|
||||
message,
|
||||
repeatDays,
|
||||
nextReminderDate,
|
||||
recipients
|
||||
recipients,
|
||||
fromDate
|
||||
}
|
||||
);
|
||||
return data.reminder;
|
||||
|
||||
@@ -2,6 +2,7 @@ export type CreateReminderDTO = {
|
||||
message?: string | null;
|
||||
repeatDays?: number | null;
|
||||
nextReminderDate?: Date | null;
|
||||
fromDate?: Date | null;
|
||||
secretId: string;
|
||||
recipients?: string[];
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -33,6 +34,7 @@ const MIN_REPEAT_DAYS = 1;
|
||||
const MAX_REPEAT_DAYS = 365;
|
||||
const DEFAULT_REPEAT_DAYS = 30;
|
||||
const DEFAULT_TEXTAREA_ROWS = 8;
|
||||
const ONE_DAY_IN_MILLIS = 86400000;
|
||||
|
||||
// Enums
|
||||
enum ReminderType {
|
||||
@@ -79,6 +81,11 @@ const ReminderFormSchema = z.object({
|
||||
.refine((data) => data > new Date(), { message: "Reminder date must be in the future" })
|
||||
.nullable()
|
||||
.optional(),
|
||||
fromDate: z.coerce
|
||||
.date()
|
||||
.refine((data) => data > new Date(), { message: "From date must be in the future" })
|
||||
.nullable()
|
||||
.optional(),
|
||||
reminderType: z.enum(["Recurring", "One Time"])
|
||||
});
|
||||
|
||||
@@ -86,7 +93,7 @@ export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
|
||||
|
||||
// Custom hook for form state management
|
||||
const useReminderForm = (reminderData?: Reminder) => {
|
||||
const { repeatDays, message, nextReminderDate } = reminderData || {};
|
||||
const { repeatDays, message, nextReminderDate, fromDate } = reminderData || {};
|
||||
|
||||
const isEditMode = Boolean(reminderData);
|
||||
|
||||
@@ -96,9 +103,10 @@ const useReminderForm = (reminderData?: Reminder) => {
|
||||
message: message || "",
|
||||
nextReminderDate: nextReminderDate || null,
|
||||
reminderType: repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||
recipients: []
|
||||
recipients: [],
|
||||
fromDate
|
||||
}),
|
||||
[repeatDays, message, nextReminderDate]
|
||||
[repeatDays, message, nextReminderDate, fromDate]
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -153,7 +161,8 @@ export const CreateReminderForm = ({
|
||||
message: reminderData?.message || "",
|
||||
nextReminderDate: reminderData?.nextReminderDate || null,
|
||||
reminderType: reminderData?.repeatDays ? ReminderType.Recurring : ReminderType.OneTime,
|
||||
recipients: []
|
||||
recipients: [],
|
||||
fromDate: reminderData?.fromDate
|
||||
},
|
||||
resolver: zodResolver(ReminderFormSchema)
|
||||
});
|
||||
@@ -170,6 +179,7 @@ export const CreateReminderForm = ({
|
||||
|
||||
// Watch form values
|
||||
const reminderType = watch("reminderType");
|
||||
const fromDate = watch("fromDate");
|
||||
|
||||
// Invalidate queries helper
|
||||
const invalidateQueries = () => {
|
||||
@@ -195,7 +205,8 @@ export const CreateReminderForm = ({
|
||||
message: data.message,
|
||||
recipients: data.recipients?.map((r) => r.value) || [],
|
||||
secretId,
|
||||
nextReminderDate: data.nextReminderDate
|
||||
nextReminderDate: data.nextReminderDate,
|
||||
fromDate: data.fromDate
|
||||
});
|
||||
|
||||
invalidateQueries();
|
||||
@@ -243,6 +254,7 @@ export const CreateReminderForm = ({
|
||||
if (newType === ReminderType.Recurring) {
|
||||
setValue("repeatDays", DEFAULT_REPEAT_DAYS);
|
||||
setValue("nextReminderDate", null);
|
||||
setValue("fromDate", null);
|
||||
} else if (newType === ReminderType.OneTime) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
@@ -259,6 +271,7 @@ export const CreateReminderForm = ({
|
||||
|
||||
const {
|
||||
repeatDays: repeatDaysInitial,
|
||||
fromDate: fromDateInitial,
|
||||
message,
|
||||
recipients,
|
||||
nextReminderDate: nextReminderDateInitial
|
||||
@@ -266,6 +279,7 @@ export const CreateReminderForm = ({
|
||||
|
||||
if (repeatDaysInitial) {
|
||||
setValue("repeatDays", repeatDaysInitial);
|
||||
setValue("fromDate", fromDateInitial);
|
||||
setValue("reminderType", ReminderType.Recurring);
|
||||
} else {
|
||||
setValue("reminderType", ReminderType.OneTime);
|
||||
@@ -323,44 +337,73 @@ export const CreateReminderForm = ({
|
||||
|
||||
{/* Conditional Fields Based on Reminder Type */}
|
||||
{reminderType === ReminderType.Recurring ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name="repeatDays"
|
||||
render={({ field, fieldState }) => (
|
||||
<div>
|
||||
<div className="grid grid-cols-[1fr,auto] gap-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
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"}
|
||||
{fromDate ? ` starting from ${format(fromDate, "MM/dd/yy")}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="fromDate"
|
||||
render={({ field, fieldState }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
className="mb-0"
|
||||
label="Reminder Interval (in days)"
|
||||
label="Start Date"
|
||||
tooltipText="When enabled, this date will be used as the start date for the first reminder"
|
||||
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);
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
className="w-full"
|
||||
onChange={field.onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isDatePickerOpen,
|
||||
onOpenChange: setIsDatePickerOpen
|
||||
}}
|
||||
type="number"
|
||||
placeholder={DEFAULT_REPEAT_DAYS.toString()}
|
||||
value={field.value || ""}
|
||||
min={MIN_REPEAT_DAYS}
|
||||
max={MAX_REPEAT_DAYS}
|
||||
popUpContentProps={{}}
|
||||
hideTime
|
||||
hidden={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
|
||||
/>
|
||||
</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>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -385,7 +428,7 @@ export const CreateReminderForm = ({
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
hideTime
|
||||
hidden={{ before: new Date(Date.now() + 86400000) }}
|
||||
hidden={{ before: new Date(Date.now() + ONE_DAY_IN_MILLIS) }}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user