From 29dfd303db4f4165e67d09b76a69e0af5a3ba4c6 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 13 Nov 2024 17:18:32 +0100 Subject: [PATCH 1/8] feat: adding workflow to send invites to the user --- actions/invitations/invitation.ts | 40 +++ .../(send-invite)/check-details/page.tsx | 3 + .../invitations/(send-invite)/layout.tsx | 32 ++ .../invitations/(send-invite)/new/page.tsx | 7 + app/(prowler)/invitations/page.tsx | 62 ++++ .../connect-account/page.tsx | 2 - .../providers/(set-up-provider)/layout.tsx | 4 +- app/(prowler)/users/page.tsx | 3 +- components/invitations/index.ts | 1 + .../invitations/send-invitation-button.tsx | 21 ++ .../invitations/workflow/forms/index.ts | 1 + .../workflow/forms/send-invitation-form.tsx | 107 +++++++ components/invitations/workflow/index.ts | 2 + .../invitations/workflow/vertical-steps.tsx | 291 ++++++++++++++++++ .../workflow/workflow-send-invite.tsx | 64 ++++ components/providers/workflow/index.ts | 2 +- ...workflow.tsx => workflow-add-provider.tsx} | 2 +- components/ui/sidebar/sidebar-items.tsx | 16 +- components/users/add-user-button.tsx | 21 ++ components/users/index.ts | 1 + 20 files changed, 664 insertions(+), 18 deletions(-) create mode 100644 actions/invitations/invitation.ts create mode 100644 app/(prowler)/invitations/(send-invite)/check-details/page.tsx create mode 100644 app/(prowler)/invitations/(send-invite)/layout.tsx create mode 100644 app/(prowler)/invitations/(send-invite)/new/page.tsx create mode 100644 app/(prowler)/invitations/page.tsx create mode 100644 components/invitations/index.ts create mode 100644 components/invitations/send-invitation-button.tsx create mode 100644 components/invitations/workflow/forms/index.ts create mode 100644 components/invitations/workflow/forms/send-invitation-form.tsx create mode 100644 components/invitations/workflow/index.ts create mode 100644 components/invitations/workflow/vertical-steps.tsx create mode 100644 components/invitations/workflow/workflow-send-invite.tsx rename components/providers/workflow/{workflow.tsx => workflow-add-provider.tsx} (98%) create mode 100644 components/users/add-user-button.tsx create mode 100644 components/users/index.ts diff --git a/actions/invitations/invitation.ts b/actions/invitations/invitation.ts new file mode 100644 index 0000000000..7fe129c92f --- /dev/null +++ b/actions/invitations/invitation.ts @@ -0,0 +1,40 @@ +"use server"; + +import { auth } from "@/auth.config"; +import { getErrorMessage, parseStringify } from "@/lib"; +export const sendInvite = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const email = formData.get("email"); + const url = new URL(`${keyServer}/tenants/invitations`); + + const body = JSON.stringify({ + data: { + type: "Invitation", + attributes: { + email, + }, + relationships: {}, + }, + }); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body, + }); + const data = await response.json(); + + return parseStringify(data); + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/app/(prowler)/invitations/(send-invite)/check-details/page.tsx b/app/(prowler)/invitations/(send-invite)/check-details/page.tsx new file mode 100644 index 0000000000..824d642093 --- /dev/null +++ b/app/(prowler)/invitations/(send-invite)/check-details/page.tsx @@ -0,0 +1,3 @@ +export default function CheckDetailsPage() { + return
CheckDetailsPage
; +} diff --git a/app/(prowler)/invitations/(send-invite)/layout.tsx b/app/(prowler)/invitations/(send-invite)/layout.tsx new file mode 100644 index 0000000000..2af2be2af2 --- /dev/null +++ b/app/(prowler)/invitations/(send-invite)/layout.tsx @@ -0,0 +1,32 @@ +import "@/styles/globals.css"; + +import { Spacer } from "@nextui-org/react"; +import React from "react"; + +import { WorkflowSendInvite } from "@/components/invitations/workflow"; +import { NavigationHeader } from "@/components/ui"; + +interface InvitationLayoutProps { + children: React.ReactNode; +} + +export default function InvitationLayout({ children }: InvitationLayoutProps) { + return ( + <> + + +
+
+ +
+
+ {children} +
+
+ + ); +} diff --git a/app/(prowler)/invitations/(send-invite)/new/page.tsx b/app/(prowler)/invitations/(send-invite)/new/page.tsx new file mode 100644 index 0000000000..87a2e33acd --- /dev/null +++ b/app/(prowler)/invitations/(send-invite)/new/page.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import { SendInvitationForm } from "@/components/invitations/workflow/forms/send-invitation-form"; + +export default function SendInvitationPage() { + return ; +} diff --git a/app/(prowler)/invitations/page.tsx b/app/(prowler)/invitations/page.tsx new file mode 100644 index 0000000000..6f0df7bc4b --- /dev/null +++ b/app/(prowler)/invitations/page.tsx @@ -0,0 +1,62 @@ +import { Spacer } from "@nextui-org/react"; +import { Suspense } from "react"; + +import { getUsers } from "@/actions/users/users"; +import { FilterControls } from "@/components/filters"; +import { filterUsers } from "@/components/filters/data-filters"; +import { SendInvitationButton } from "@/components/invitations"; +import { Header } from "@/components/ui"; +import { DataTable } from "@/components/ui/table"; +import { ColumnsUser, SkeletonTableUser } from "@/components/users/table"; +import { SearchParamsProps } from "@/types"; + +export default async function Invitations({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); + + return ( + <> +
+ + + + + + + }> + + + + ); +} + +const SSRDataTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const usersData = await getUsers({ query, page, sort, filters }); + + return ( + + ); +}; diff --git a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx index 67094101f5..eead6c2561 100644 --- a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx @@ -1,5 +1,3 @@ -"use client"; - import React from "react"; import { ConnectAccountForm } from "@/components/providers/workflow/forms"; diff --git a/app/(prowler)/providers/(set-up-provider)/layout.tsx b/app/(prowler)/providers/(set-up-provider)/layout.tsx index 788ce9cd96..85b78ee687 100644 --- a/app/(prowler)/providers/(set-up-provider)/layout.tsx +++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx @@ -3,7 +3,7 @@ import "@/styles/globals.css"; import { Spacer } from "@nextui-org/react"; import React from "react"; -import { Workflow } from "@/components/providers/workflow"; +import { WorkflowAddProvider } from "@/components/providers/workflow"; import { NavigationHeader } from "@/components/ui"; interface ProviderLayoutProps { @@ -21,7 +21,7 @@ export default function ProviderLayout({ children }: ProviderLayoutProps) {
- +
{children} diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx index fdb6f0de03..805c8271e6 100644 --- a/app/(prowler)/users/page.tsx +++ b/app/(prowler)/users/page.tsx @@ -6,6 +6,7 @@ import { FilterControls } from "@/components/filters"; import { filterUsers } from "@/components/filters/data-filters"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; +import { AddUserButton } from "@/components/users"; import { ColumnsUser, SkeletonTableUser } from "@/components/users/table"; import { SearchParamsProps } from "@/types"; @@ -22,7 +23,7 @@ export default async function Users({ - {/* */} + }> diff --git a/components/invitations/index.ts b/components/invitations/index.ts new file mode 100644 index 0000000000..7eabbf93fe --- /dev/null +++ b/components/invitations/index.ts @@ -0,0 +1 @@ +export * from "./send-invitation-button"; diff --git a/components/invitations/send-invitation-button.tsx b/components/invitations/send-invitation-button.tsx new file mode 100644 index 0000000000..ae13d33b1d --- /dev/null +++ b/components/invitations/send-invitation-button.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { AddIcon } from "../icons"; +import { CustomButton } from "../ui/custom"; + +export const SendInvitationButton = () => { + return ( +
+ } + > + Send Invitation + +
+ ); +}; diff --git a/components/invitations/workflow/forms/index.ts b/components/invitations/workflow/forms/index.ts new file mode 100644 index 0000000000..3180c95b40 --- /dev/null +++ b/components/invitations/workflow/forms/index.ts @@ -0,0 +1 @@ +export * from "./send-invitation-form"; diff --git a/components/invitations/workflow/forms/send-invitation-form.tsx b/components/invitations/workflow/forms/send-invitation-form.tsx new file mode 100644 index 0000000000..39dd42803b --- /dev/null +++ b/components/invitations/workflow/forms/send-invitation-form.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { sendInvite } from "@/actions/invitations/invitation"; +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { ApiError } from "@/types"; + +const sendInvitationFormSchema = z.object({ + email: z.string().email("Please enter a valid email"), +}); + +export type FormValues = z.infer; + +export const SendInvitationForm = () => { + const { toast } = useToast(); + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(sendInvitationFormSchema), + defaultValues: { + email: "", + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: FormValues) => { + const formData = new FormData(); + formData.append("email", values.email); + + try { + const data = await sendInvite(formData); + + if (data?.errors && data.errors.length > 0) { + data.errors.forEach((error: ApiError) => { + const errorMessage = error.detail; + switch (error.source.pointer) { + case "/data/attributes/email": + form.setError("email", { + type: "server", + message: errorMessage, + }); + break; + default: + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } + }); + } else { + const token = data?.data?.attributes.token || ""; + router.push(`invitations/check-details/?invitation_token=${token}`); + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error", + description: "An unexpected error occurred. Please try again.", + }); + } + }; + + return ( +
+ + + +
+ } + > + {isLoading ? <>Loading : Send Invitation} + +
+ + + ); +}; diff --git a/components/invitations/workflow/index.ts b/components/invitations/workflow/index.ts new file mode 100644 index 0000000000..41589b2616 --- /dev/null +++ b/components/invitations/workflow/index.ts @@ -0,0 +1,2 @@ +export * from "./vertical-steps"; +export * from "./workflow-send-invite"; diff --git a/components/invitations/workflow/vertical-steps.tsx b/components/invitations/workflow/vertical-steps.tsx new file mode 100644 index 0000000000..74eaa32747 --- /dev/null +++ b/components/invitations/workflow/vertical-steps.tsx @@ -0,0 +1,291 @@ +"use client"; + +import type { ButtonProps } from "@nextui-org/react"; +import { cn } from "@nextui-org/react"; +import { useControlledState } from "@react-stately/utils"; +import { domAnimation, LazyMotion, m } from "framer-motion"; +import type { ComponentProps } from "react"; +import React from "react"; + +export type VerticalStepProps = { + className?: string; + description?: React.ReactNode; + title?: React.ReactNode; +}; + +export interface VerticalStepsProps + extends React.HTMLAttributes { + /** + * An array of steps. + * + * @default [] + */ + steps?: VerticalStepProps[]; + /** + * The color of the steps. + * + * @default "primary" + */ + color?: ButtonProps["color"]; + /** + * The current step index. + */ + currentStep?: number; + /** + * The default step index. + * + * @default 0 + */ + defaultStep?: number; + /** + * Whether to hide the progress bars. + * + * @default false + */ + hideProgressBars?: boolean; + /** + * The custom class for the steps wrapper. + */ + className?: string; + /** + * The custom class for the step. + */ + stepClassName?: string; + /** + * Callback function when the step index changes. + */ + onStepChange?: (stepIndex: number) => void; +} + +function CheckIcon(props: ComponentProps<"svg">) { + return ( + + + + ); +} + +export const VerticalSteps = React.forwardRef< + HTMLButtonElement, + VerticalStepsProps +>( + ( + { + color = "primary", + steps = [], + defaultStep = 0, + onStepChange, + currentStep: currentStepProp, + hideProgressBars = false, + stepClassName, + className, + ...props + }, + ref, + ) => { + const [currentStep, setCurrentStep] = useControlledState( + currentStepProp, + defaultStep, + onStepChange, + ); + + const colors = React.useMemo(() => { + let userColor; + let fgColor; + + const colorsVars = [ + "[--active-fg-color:var(--step-fg-color)]", + "[--active-border-color:var(--step-color)]", + "[--active-color:var(--step-color)]", + "[--complete-background-color:var(--step-color)]", + "[--complete-border-color:var(--step-color)]", + "[--inactive-border-color:hsl(var(--nextui-default-300))]", + "[--inactive-color:hsl(var(--nextui-default-300))]", + ]; + + switch (color) { + case "primary": + userColor = "[--step-color:hsl(var(--nextui-primary))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]"; + break; + case "secondary": + userColor = "[--step-color:hsl(var(--nextui-secondary))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-secondary-foreground))]"; + break; + case "success": + userColor = "[--step-color:hsl(var(--nextui-success))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-success-foreground))]"; + break; + case "warning": + userColor = "[--step-color:hsl(var(--nextui-warning))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-warning-foreground))]"; + break; + case "danger": + userColor = "[--step-color:hsl(var(--nextui-error))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-error-foreground))]"; + break; + case "default": + userColor = "[--step-color:hsl(var(--nextui-default))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-default-foreground))]"; + break; + default: + userColor = "[--step-color:hsl(var(--nextui-primary))]"; + fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]"; + break; + } + + if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor); + if (!className?.includes("--step-color")) colorsVars.unshift(userColor); + if (!className?.includes("--inactive-bar-color")) + colorsVars.push( + "[--inactive-bar-color:hsl(var(--nextui-default-300))]", + ); + + return colorsVars; + }, [color, className]); + + return ( + + ); + }, +); + +VerticalSteps.displayName = "VerticalSteps"; diff --git a/components/invitations/workflow/workflow-send-invite.tsx b/components/invitations/workflow/workflow-send-invite.tsx new file mode 100644 index 0000000000..cf87d04f06 --- /dev/null +++ b/components/invitations/workflow/workflow-send-invite.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Progress, Spacer } from "@nextui-org/react"; +import { usePathname } from "next/navigation"; +import React from "react"; + +import { VerticalSteps } from "./vertical-steps"; + +const steps = [ + { + title: "Send Invitation", + description: + "Enter the email address of the person you want to invite and send the invitation.", + href: "/invitations/new", + }, + { + title: "Review Invitation Details", + description: + "Review the invitation details and share the information required for the person to accept the invitation.", + href: "/invitations/check-details", + }, +]; + +export const WorkflowSendInvite = () => { + const pathname = usePathname(); + + // Calculate current step based on pathname + const currentStepIndex = steps.findIndex((step) => + pathname.endsWith(step.href), + ); + const currentStep = currentStepIndex === -1 ? 0 : currentStepIndex; + + return ( +
+

+ Send invitation +

+

+ Follow the steps to send an invitation to the users. +

+ + + +
+ ); +}; diff --git a/components/providers/workflow/index.ts b/components/providers/workflow/index.ts index f245196eab..fca9e92617 100644 --- a/components/providers/workflow/index.ts +++ b/components/providers/workflow/index.ts @@ -1,2 +1,2 @@ export * from "./vertical-steps"; -export * from "./workflow"; +export * from "./workflow-add-provider"; diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow-add-provider.tsx similarity index 98% rename from components/providers/workflow/workflow.tsx rename to components/providers/workflow/workflow-add-provider.tsx index da5fa18f99..69bc885638 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow-add-provider.tsx @@ -32,7 +32,7 @@ const steps = [ }, ]; -export const Workflow = () => { +export const WorkflowAddProvider = () => { const pathname = usePathname(); // Calculate current step based on pathname diff --git a/components/ui/sidebar/sidebar-items.tsx b/components/ui/sidebar/sidebar-items.tsx index 32043aadc9..a4fae9eab4 100644 --- a/components/ui/sidebar/sidebar-items.tsx +++ b/components/ui/sidebar/sidebar-items.tsx @@ -29,10 +29,10 @@ export const items: SidebarItem[] = [ ), }, { - key: "tasks", + key: "invitations", href: "#", icon: "solar:checklist-minimalistic-outline", - title: "Tasks", + title: "Invitations", endContent: ( , - // }, ], }, ]; diff --git a/components/users/add-user-button.tsx b/components/users/add-user-button.tsx new file mode 100644 index 0000000000..b4a63fc7a7 --- /dev/null +++ b/components/users/add-user-button.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { AddIcon } from "../icons"; +import { CustomButton } from "../ui/custom"; + +export const AddUserButton = () => { + return ( +
+ } + > + Invite User + +
+ ); +}; diff --git a/components/users/index.ts b/components/users/index.ts new file mode 100644 index 0000000000..014350e9a6 --- /dev/null +++ b/components/users/index.ts @@ -0,0 +1 @@ +export * from "./add-user-button"; From 890bd12e995cbdd76df6e41335ccf3b79d3a8089 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 13 Nov 2024 18:52:06 +0100 Subject: [PATCH 2/8] feat: workflow to invite an user is working --- actions/invitations/invitation.ts | 24 ++++ .../(send-invite)/check-details/page.tsx | 44 ++++++- components/invitations/index.ts | 1 + components/invitations/invitation-details.tsx | 112 ++++++++++++++++++ .../workflow/forms/send-invitation-form.tsx | 4 +- components/invitations/workflow/index.ts | 1 + .../workflow/skeleton-invitation-info.tsx | 65 ++++++++++ types/components.ts | 24 ++++ 8 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 components/invitations/invitation-details.tsx create mode 100644 components/invitations/workflow/skeleton-invitation-info.tsx diff --git a/actions/invitations/invitation.ts b/actions/invitations/invitation.ts index 7fe129c92f..1a11900422 100644 --- a/actions/invitations/invitation.ts +++ b/actions/invitations/invitation.ts @@ -2,6 +2,7 @@ import { auth } from "@/auth.config"; import { getErrorMessage, parseStringify } from "@/lib"; + export const sendInvite = async (formData: FormData) => { const session = await auth(); const keyServer = process.env.API_BASE_URL; @@ -38,3 +39,26 @@ export const sendInvite = async (formData: FormData) => { }; } }; + +export const getInvitationInfoById = async (invitationId: string) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + const data = await response.json(); + return parseStringify(data); + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/app/(prowler)/invitations/(send-invite)/check-details/page.tsx b/app/(prowler)/invitations/(send-invite)/check-details/page.tsx index 824d642093..939667cdc1 100644 --- a/app/(prowler)/invitations/(send-invite)/check-details/page.tsx +++ b/app/(prowler)/invitations/(send-invite)/check-details/page.tsx @@ -1,3 +1,43 @@ -export default function CheckDetailsPage() { - return
CheckDetailsPage
; +import { Suspense } from "react"; + +import { getInvitationInfoById } from "@/actions/invitations/invitation"; +import { InvitationDetails } from "@/components/invitations"; +import { SkeletonInvitationInfo } from "@/components/invitations/workflow"; +import { SearchParamsProps } from "@/types"; + +export default async function CheckDetailsPage({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); + + return ( + }> + + + ); } + +const SSRDataInvitation = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const invitationId = searchParams.id; + + if (!invitationId) { + return
Invalid invitation ID
; + } + + const invitationData = (await getInvitationInfoById(invitationId as string)) + .data; + + if (!invitationData) { + return
Invitation not found
; + } + + const { attributes, links } = invitationData; + + return ; +}; diff --git a/components/invitations/index.ts b/components/invitations/index.ts index 7eabbf93fe..e381a114df 100644 --- a/components/invitations/index.ts +++ b/components/invitations/index.ts @@ -1 +1,2 @@ +export * from "./invitation-details"; export * from "./send-invitation-button"; diff --git a/components/invitations/invitation-details.tsx b/components/invitations/invitation-details.tsx new file mode 100644 index 0000000000..12c6209920 --- /dev/null +++ b/components/invitations/invitation-details.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { Card, CardBody, Divider, Snippet } from "@nextui-org/react"; + +import { AddIcon } from "../icons"; +import { CustomButton } from "../ui/custom"; +import { DateWithTime } from "../ui/entities"; + +interface InvitationDetailsProps { + attributes: { + email: string; + state: string; + token: string; + expires_at: string; + inserted_at: string; + updated_at: string; + }; + relationships?: { + inviter: { + data: { + id: string; + }; + }; + }; + selfLink: string; +} + +export const InvitationDetails = ({ + attributes, + selfLink, +}: InvitationDetailsProps) => { + return ( +
+ + +

+ Invitation Details +

+ + +
+
+ Email: + {attributes.email} +
+ +
+ State: + {attributes.state} +
+ +
+ Token: + {attributes.token} +
+ +
+ Expires At: + +
+ +
+ Inserted At: + +
+ +
+ Updated At: + +
+
+ + +

+ Share this link with the user: +

+ +
+ +

+ {selfLink} +

+
+
+
+
+
+ } + > + Back to Invitations + +
+
+ ); +}; diff --git a/components/invitations/workflow/forms/send-invitation-form.tsx b/components/invitations/workflow/forms/send-invitation-form.tsx index 39dd42803b..a75c6a60c6 100644 --- a/components/invitations/workflow/forms/send-invitation-form.tsx +++ b/components/invitations/workflow/forms/send-invitation-form.tsx @@ -57,8 +57,8 @@ export const SendInvitationForm = () => { } }); } else { - const token = data?.data?.attributes.token || ""; - router.push(`invitations/check-details/?invitation_token=${token}`); + const invitationId = data?.data?.id || ""; + router.push(`/invitations/check-details/?id=${invitationId}`); } } catch (error) { toast({ diff --git a/components/invitations/workflow/index.ts b/components/invitations/workflow/index.ts index 41589b2616..a9a860f213 100644 --- a/components/invitations/workflow/index.ts +++ b/components/invitations/workflow/index.ts @@ -1,2 +1,3 @@ +export * from "./skeleton-invitation-info"; export * from "./vertical-steps"; export * from "./workflow-send-invite"; diff --git a/components/invitations/workflow/skeleton-invitation-info.tsx b/components/invitations/workflow/skeleton-invitation-info.tsx new file mode 100644 index 0000000000..12abeb9da7 --- /dev/null +++ b/components/invitations/workflow/skeleton-invitation-info.tsx @@ -0,0 +1,65 @@ +import { Card, Skeleton } from "@nextui-org/react"; +import React from "react"; + +export const SkeletonInvitationInfo = () => { + return ( + + {/* Table headers */} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Table body */} +
+ {[...Array(3)].map((_, index) => ( +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ ); +}; diff --git a/types/components.ts b/types/components.ts index d753bac9d9..2299d0bffe 100644 --- a/types/components.ts +++ b/types/components.ts @@ -101,6 +101,30 @@ export interface ApiError { code: string; } +export interface InvitationProps { + type: "Invitation"; + id: string; + attributes: { + inserted_at: string; + updated_at: string; + email: string; + state: string; + token: string; + expires_at: string; + }; + relationships: { + inviter: { + data: { + type: "User"; + id: string; + }; + }; + }; + links: { + self: string; + }; +} + export interface UserProps { type: "User"; id: string; From 1dc4bd313a7fd097f2a245cea6ac3b81df5d3eb6 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 14 Nov 2024 08:08:08 +0100 Subject: [PATCH 3/8] feat: invitation workflow is working as expected --- actions/invitations/invitation.ts | 110 +++++++++++++++- app/(prowler)/invitations/page.tsx | 21 ++-- components/filters/data-filters.ts | 8 ++ components/invitations/forms/delete-form.tsx | 95 ++++++++++++++ components/invitations/forms/edit-form.tsx | 118 ++++++++++++++++++ components/invitations/forms/index.ts | 2 + .../invitations/table/column-invitations.tsx | 72 +++++++++++ .../table/data-table-row-actions.tsx | 117 +++++++++++++++++ components/invitations/table/index.ts | 3 + .../table/skeleton-table-invitations.tsx | 59 +++++++++ types/formSchemas.ts | 6 + 11 files changed, 601 insertions(+), 10 deletions(-) create mode 100644 components/invitations/forms/delete-form.tsx create mode 100644 components/invitations/forms/edit-form.tsx create mode 100644 components/invitations/forms/index.ts create mode 100644 components/invitations/table/column-invitations.tsx create mode 100644 components/invitations/table/data-table-row-actions.tsx create mode 100644 components/invitations/table/index.ts create mode 100644 components/invitations/table/skeleton-table-invitations.tsx diff --git a/actions/invitations/invitation.ts b/actions/invitations/invitation.ts index 1a11900422..7fc99ce33c 100644 --- a/actions/invitations/invitation.ts +++ b/actions/invitations/invitation.ts @@ -1,7 +1,51 @@ "use server"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + import { auth } from "@/auth.config"; -import { getErrorMessage, parseStringify } from "@/lib"; +import { getErrorMessage, parseStringify, wait } from "@/lib"; + +export const getInvitations = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/invitations"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/tenants/invitations`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const invitations = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await invitations.json(); + const parsedData = parseStringify(data); + revalidatePath("/invitations"); + return parsedData; + } catch (error) { + console.error("Error fetching invitations:", error); + return undefined; + } +}; export const sendInvite = async (formData: FormData) => { const session = await auth(); @@ -40,6 +84,46 @@ export const sendInvite = async (formData: FormData) => { } }; +export const updateInvite = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const invitationId = formData.get("invitationId"); + const invitationEmail = formData.get("invitationEmail"); + const expiresAt = formData.get("expires_at"); + + const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify({ + data: { + type: "Invitation", + id: invitationId, + attributes: { + email: invitationEmail, + ...(expiresAt && { expires_at: expiresAt }), + }, + }, + }), + }); + const data = await response.json(); + revalidatePath("/invitations"); + return parseStringify(data); + } catch (error) { + console.error("Error updating invitation:", error); + return { + error: getErrorMessage(error), + }; + } +}; + export const getInvitationInfoById = async (invitationId: string) => { const session = await auth(); const keyServer = process.env.API_BASE_URL; @@ -62,3 +146,27 @@ export const getInvitationInfoById = async (invitationId: string) => { }; } }; + +export const revokeInvite = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const invitationId = formData.get("invitationId"); + const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + try { + const response = await fetch(url.toString(), { + method: "DELETE", + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await response.json(); + await wait(1000); + revalidatePath("/invitations"); + return parseStringify(data); + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/app/(prowler)/invitations/page.tsx b/app/(prowler)/invitations/page.tsx index 6f0df7bc4b..b32573cd45 100644 --- a/app/(prowler)/invitations/page.tsx +++ b/app/(prowler)/invitations/page.tsx @@ -1,13 +1,16 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; -import { getUsers } from "@/actions/users/users"; +import { getInvitations } from "@/actions/invitations/invitation"; import { FilterControls } from "@/components/filters"; -import { filterUsers } from "@/components/filters/data-filters"; +import { filterInvitations } from "@/components/filters/data-filters"; import { SendInvitationButton } from "@/components/invitations"; +import { + ColumnsInvitation, + SkeletonTableInvitation, +} from "@/components/invitations/table"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; -import { ColumnsUser, SkeletonTableUser } from "@/components/users/table"; import { SearchParamsProps } from "@/types"; export default async function Invitations({ @@ -26,7 +29,7 @@ export default async function Invitations({ - }> + }> @@ -49,14 +52,14 @@ const SSRDataTable = async ({ // Extract query from filters const query = (filters["filter[search]"] as string) || ""; - const usersData = await getUsers({ query, page, sort, filters }); + const invitationsData = await getInvitations({ query, page, sort, filters }); return ( ); }; diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts index 21691b2c5c..91a039605d 100644 --- a/components/filters/data-filters.ts +++ b/components/filters/data-filters.ts @@ -59,3 +59,11 @@ export const filterUsers = [ values: ["true", "false"], }, ]; + +export const filterInvitations = [ + { + key: "state", + labelCheckboxGroup: "State", + values: ["pending", "accepted", "expired", "revoked"], + }, +]; diff --git a/components/invitations/forms/delete-form.tsx b/components/invitations/forms/delete-form.tsx new file mode 100644 index 0000000000..35f81bbaad --- /dev/null +++ b/components/invitations/forms/delete-form.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { Dispatch, SetStateAction } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { revokeInvite } from "@/actions/invitations/invitation"; +import { DeleteIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; + +const formSchema = z.object({ + invitationId: z.string(), +}); + +export const DeleteForm = ({ + invitationId, + setIsOpen, +}: { + invitationId: string; + setIsOpen: Dispatch>; +}) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + invitationId, + }, + }); + const { toast } = useToast(); + const isLoading = form.formState.isSubmitting; + + async function onSubmitClient(values: z.infer) { + const formData = new FormData(); + + Object.entries(values).forEach( + ([key, value]) => value !== undefined && formData.append(key, value), + ); + // client-side validation + const data = await revokeInvite(formData); + + if (data?.errors && data.errors.length > 0) { + const error = data.errors[0]; + const errorMessage = `${error.detail}`; + // show error + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } else { + toast({ + title: "Success!", + description: "The invitation was revoked successfully.", + }); + } + setIsOpen(false); // Close the modal on success + } + + return ( +
+ + +
+ setIsOpen(false)} + isDisabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Delete} + +
+
+ + ); +}; diff --git a/components/invitations/forms/edit-form.tsx b/components/invitations/forms/edit-form.tsx new file mode 100644 index 0000000000..17924cea97 --- /dev/null +++ b/components/invitations/forms/edit-form.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Dispatch, SetStateAction } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { updateInvite } from "@/actions/invitations/invitation"; +import { SaveIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { editInviteFormSchema } from "@/types"; + +export const EditForm = ({ + invitationId, + invitationEmail, + setIsOpen, +}: { + invitationId: string; + invitationEmail?: string; + setIsOpen: Dispatch>; +}) => { + const formSchema = editInviteFormSchema; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + invitationId, + invitationEmail: invitationEmail, + }, + }); + + const { toast } = useToast(); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: z.infer) => { + const formData = new FormData(); + console.log(values); + + Object.entries(values).forEach( + ([key, value]) => value !== undefined && formData.append(key, value), + ); + + const data = await updateInvite(formData); + + if (data?.error) { + const errorMessage = `${data.error}`; + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } else { + toast({ + title: "Success!", + description: "The invitation was updated successfully.", + }); + setIsOpen(false); // Close the modal on success + } + }; + + return ( +
+ +
+ Current email: {invitationEmail} +
+
+ +
+ + +
+ setIsOpen(false)} + isDisabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Save} + +
+
+ + ); +}; diff --git a/components/invitations/forms/index.ts b/components/invitations/forms/index.ts new file mode 100644 index 0000000000..a081952ade --- /dev/null +++ b/components/invitations/forms/index.ts @@ -0,0 +1,2 @@ +export * from "./delete-form"; +export * from "./edit-form"; diff --git a/components/invitations/table/column-invitations.tsx b/components/invitations/table/column-invitations.tsx new file mode 100644 index 0000000000..9e30eb8024 --- /dev/null +++ b/components/invitations/table/column-invitations.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DateWithTime } from "@/components/ui/entities"; +import { DataTableColumnHeader } from "@/components/ui/table"; +import { InvitationProps } from "@/types"; + +import { DataTableRowActions } from "./data-table-row-actions"; + +const getInvitationData = (row: { original: InvitationProps }) => { + return row.original.attributes; +}; + +export const ColumnsInvitation: ColumnDef[] = [ + { + accessorKey: "email", + header: () =>
Email
, + cell: ({ row }) => { + const data = getInvitationData(row); + return

{data?.email || "N/A"}

; + }, + }, + { + accessorKey: "state", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { state } = getInvitationData(row); + return

{state}

; + }, + }, + { + accessorKey: "inserted_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { inserted_at } = getInvitationData(row); + return ; + }, + }, + + { + accessorKey: "expires_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { expires_at } = getInvitationData(row); + return ; + }, + }, + + { + accessorKey: "actions", + header: () =>
Actions
, + id: "actions", + cell: ({ row }) => { + return ; + }, + }, +]; diff --git a/components/invitations/table/data-table-row-actions.tsx b/components/invitations/table/data-table-row-actions.tsx new file mode 100644 index 0000000000..580ffaeb0c --- /dev/null +++ b/components/invitations/table/data-table-row-actions.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { + Button, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownSection, + DropdownTrigger, +} from "@nextui-org/react"; +import { + AddNoteBulkIcon, + DeleteDocumentBulkIcon, + EditDocumentBulkIcon, +} from "@nextui-org/shared-icons"; +import { Row } from "@tanstack/react-table"; +import clsx from "clsx"; +import { useState } from "react"; + +import { VerticalDotsIcon } from "@/components/icons"; +import { CustomAlertModal } from "@/components/ui/custom"; + +import { DeleteForm, EditForm } from "../forms"; + +interface DataTableRowActionsProps { + row: Row; +} +const iconClasses = + "text-2xl text-default-500 pointer-events-none flex-shrink-0"; + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + const [isEditOpen, setIsEditOpen] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const invitationId = (row.original as { id: string }).id; + const invitationEmail = (row.original as any).attributes?.email; + return ( + <> + + + + + + + +
+ + + + + + + } + > + Check Details + + + } + onClick={() => setIsEditOpen(true)} + > + Edit Invitation + + + + + } + onClick={() => setIsDeleteOpen(true)} + > + Delete Invitation + + + + +
+ + ); +} diff --git a/components/invitations/table/index.ts b/components/invitations/table/index.ts new file mode 100644 index 0000000000..177edf2bb9 --- /dev/null +++ b/components/invitations/table/index.ts @@ -0,0 +1,3 @@ +export * from "./column-invitations"; +export * from "./data-table-row-actions"; +export * from "./skeleton-table-invitations"; diff --git a/components/invitations/table/skeleton-table-invitations.tsx b/components/invitations/table/skeleton-table-invitations.tsx new file mode 100644 index 0000000000..6d18313fe4 --- /dev/null +++ b/components/invitations/table/skeleton-table-invitations.tsx @@ -0,0 +1,59 @@ +import { Card, Skeleton } from "@nextui-org/react"; +import React from "react"; + +export const SkeletonTableInvitation = () => { + return ( + + {/* Table headers */} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Table body */} +
+ {[...Array(10)].map((_, index) => ( +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ ); +}; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index 333064cfa4..4238df3ef2 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -151,6 +151,12 @@ export const editProviderFormSchema = (currentAlias: string) => providerId: z.string(), }); +export const editInviteFormSchema = z.object({ + invitationId: z.string().uuid(), + invitationEmail: z.string().email(), + expires_at: z.string().optional(), +}); + export const editUserFormSchema = ( currentName: string, currentEmail: string, From 58068b34bf5bf6d679776a5133bf84f7a543e215 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 14 Nov 2024 11:55:11 +0100 Subject: [PATCH 4/8] feat: invitations are working - first iteration --- actions/auth/auth.ts | 5 ++++- app/(auth)/sign-up/page.tsx | 10 ++++++++-- components/auth/oss/auth-form.tsx | 25 ++++++++++++++++++++++++- types/authFormSchema.ts | 2 ++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/actions/auth/auth.ts b/actions/auth/auth.ts index e0de4422b6..4973e3f8e2 100644 --- a/actions/auth/auth.ts +++ b/actions/auth/auth.ts @@ -56,6 +56,10 @@ export const createNewUser = async ( const keyServer = process.env.API_BASE_URL; const url = new URL(`${keyServer}/users`); + if (formData.invitationToken) { + url.searchParams.append("invitation_token", formData.invitationToken); + } + const bodyData = { data: { type: "User", @@ -79,7 +83,6 @@ export const createNewUser = async ( }); const parsedResponse = await response.json(); - if (!response.ok) { return parsedResponse; } diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx index 8e525507b8..28149bb200 100644 --- a/app/(auth)/sign-up/page.tsx +++ b/app/(auth)/sign-up/page.tsx @@ -1,7 +1,13 @@ import { AuthForm } from "@/components/auth/oss"; +import { SearchParamsProps } from "@/types"; -const SignUp = () => { - return ; +const SignUp = ({ searchParams }: { searchParams: SearchParamsProps }) => { + const invitationToken = + typeof searchParams?.invitation_token === "string" + ? searchParams.invitation_token + : null; + + return ; }; export default SignUp; diff --git a/components/auth/oss/auth-form.tsx b/components/auth/oss/auth-form.tsx index 2afe281f2a..1be8e9e5e5 100644 --- a/components/auth/oss/auth-form.tsx +++ b/components/auth/oss/auth-form.tsx @@ -20,7 +20,13 @@ import { } from "@/components/ui/form"; import { ApiError, authFormSchema } from "@/types"; -export const AuthForm = ({ type }: { type: string }) => { +export const AuthForm = ({ + type, + invitationToken, +}: { + type: string; + invitationToken?: string | null; +}) => { const formSchema = authFormSchema(type); const router = useRouter(); @@ -96,6 +102,12 @@ export const AuthForm = ({ type }: { type: string }) => { message: errorMessage, }); break; + case "/data/attributes/invitation_token": + form.setError("invitationToken", { + type: "server", + message: errorMessage, + }); + break; default: toast({ variant: "destructive", @@ -186,6 +198,17 @@ export const AuthForm = ({ type }: { type: string }) => { name="confirmPassword" confirmPassword /> + {invitationToken && ( + + )} : z.string().min(12, { message: "It must contain at least 12 characters.", }), + invitationToken: + type === "sign-in" ? z.string().optional() : z.string().optional(), termsAndConditions: type === "sign-in" ? z.boolean().optional() From e21386c1d5229062f1edf7c3fb136e0d2fe2f8b5 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 12:48:23 +0100 Subject: [PATCH 5/8] chore: Show the error in the after the invitation token field --- components/auth/oss/auth-form.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/auth/oss/auth-form.tsx b/components/auth/oss/auth-form.tsx index 1be8e9e5e5..1f1ec4384f 100644 --- a/components/auth/oss/auth-form.tsx +++ b/components/auth/oss/auth-form.tsx @@ -40,6 +40,7 @@ export const AuthForm = ({ company: "", termsAndConditions: false, confirmPassword: "", + ...(invitationToken && { invitationToken }), }), }, }); @@ -102,7 +103,7 @@ export const AuthForm = ({ message: errorMessage, }); break; - case "/data/attributes/invitation_token": + case "/data": form.setError("invitationToken", { type: "server", message: errorMessage, @@ -205,6 +206,7 @@ export const AuthForm = ({ type="text" label="Invitation Token" placeholder={invitationToken} + defaultValue={invitationToken} isRequired={false} isInvalid={!!form.formState.errors.invitationToken} /> From 4fd5d868c617f7cda981c27fbf4cd64aa1c4ba99 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 12:49:33 +0100 Subject: [PATCH 6/8] chore: change label for revoke invitations --- components/invitations/invitation-details.tsx | 9 ++++----- components/invitations/table/data-table-row-actions.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/components/invitations/invitation-details.tsx b/components/invitations/invitation-details.tsx index 12c6209920..7739cf8adb 100644 --- a/components/invitations/invitation-details.tsx +++ b/components/invitations/invitation-details.tsx @@ -25,10 +25,9 @@ interface InvitationDetailsProps { selfLink: string; } -export const InvitationDetails = ({ - attributes, - selfLink, -}: InvitationDetailsProps) => { +export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => { + const baseURL = process.env.SITE_URL || "http://localhost:3000"; + const invitationLink = `${baseURL}/sign-up?invitation_token=${attributes.token}`; return (

- {selfLink} + {invitationLink}

diff --git a/components/invitations/table/data-table-row-actions.tsx b/components/invitations/table/data-table-row-actions.tsx index 580ffaeb0c..b743f8aeba 100644 --- a/components/invitations/table/data-table-row-actions.tsx +++ b/components/invitations/table/data-table-row-actions.tsx @@ -106,7 +106,7 @@ export function DataTableRowActions({ } onClick={() => setIsDeleteOpen(true)} > - Delete Invitation + Revoke Invitation From 3f5f50fe380019774706ffdfda995623e6e01b68 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 12:50:22 +0100 Subject: [PATCH 7/8] chore: add defaultValue prop to the CustomInput component --- components/ui/custom/custom-input.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/ui/custom/custom-input.tsx b/components/ui/custom/custom-input.tsx index a391142344..2b390b83c0 100644 --- a/components/ui/custom/custom-input.tsx +++ b/components/ui/custom/custom-input.tsx @@ -18,6 +18,7 @@ interface CustomInputProps { placeholder?: string; password?: boolean; confirmPassword?: boolean; + defaultValue?: string; isRequired?: boolean; isInvalid?: boolean; } @@ -33,6 +34,7 @@ export const CustomInput = ({ size = "md", confirmPassword = false, password = false, + defaultValue, isRequired = true, isInvalid, }: CustomInputProps) => { @@ -99,6 +101,7 @@ export const CustomInput = ({ variant={variant} size={size} isInvalid={isInvalid} + defaultValue={defaultValue} endContent={endContent} {...field} /> From 01bc745478052bd89f79c609534becbf9ab1f9f6 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 12:59:30 +0100 Subject: [PATCH 8/8] chore: replace 'delete' with 'revoke' in invitations --- components/invitations/forms/delete-form.tsx | 4 ++-- components/invitations/table/data-table-row-actions.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/invitations/forms/delete-form.tsx b/components/invitations/forms/delete-form.tsx index 35f81bbaad..bea018b2ae 100644 --- a/components/invitations/forms/delete-form.tsx +++ b/components/invitations/forms/delete-form.tsx @@ -78,7 +78,7 @@ export const DeleteForm = ({ } > - {isLoading ? <>Loading : Delete} + {isLoading ? <>Loading : Revoke}
diff --git a/components/invitations/table/data-table-row-actions.tsx b/components/invitations/table/data-table-row-actions.tsx index b743f8aeba..2a17c944dc 100644 --- a/components/invitations/table/data-table-row-actions.tsx +++ b/components/invitations/table/data-table-row-actions.tsx @@ -53,7 +53,7 @@ export function DataTableRowActions({ isOpen={isDeleteOpen} onOpenChange={setIsDeleteOpen} title="Are you absolutely sure?" - description="This action cannot be undone. This will permanently delete your invitation." + description="This action cannot be undone. This will permanently revoke your invitation." >