chore: refactor auth provider form to use react-hook-form (#44095)

## Problem

- The auth providers forms still use `formik` and we want to remove it
in favour of `react-hook-form` to keep only one form library
- The auth providers forms do not follow the design system guidelines

## Solution

- Migrate to `react-hook-form`
- Apply the design system guidelines

## Screenshots

<img width="1530" height="1920" alt="image"
src="https://github.com/user-attachments/assets/04627e93-2aa5-4a7f-a24e-0ae41d6e6b10"
/>

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
This commit is contained in:
Gildas GarciaandDanny White authored and GitHub committed 2026-03-25 09:29:28 +01:00
1 parent a261bafbe2
commit 6b35cc8034
4 files changed
+499 -395

No files matched your search

@@ -1,52 +1,86 @@
import { Markdown } from 'components/interfaces/Markdown'
import { DatePicker } from 'components/ui/DatePicker'
import dayjs from 'dayjs'
import { format } from 'date-fns'
import { BASE_PATH } from 'lib/constants'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { CalendarIcon, ExternalLink } from 'lucide-react'
import { type Control } from 'react-hook-form'
import ReactMarkdown from 'react-markdown'
import { Button, Input, InputNumber, Listbox, Toggle } from 'ui'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import {
Button,
Calendar,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
PrePostTab,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Separator,
SheetSection,
Switch,
Textarea,
useWatch_Shadcn_,
} from 'ui'
import { Input as DataInput } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import type { Enum } from './AuthProvidersForm.types'
interface FormFieldProps {
projectRef: string | undefined
organizationSlug: string | undefined
name: string
properties: any
formValues: any
setFieldValue: (field: string, v: any) => any
control: Control
hasAccess: boolean
disabled?: boolean
}
function formatDate(date: Date): string {
return dayjs(date).format('dddd, MMMM D, YYYY HH:mm:ss Z')
}
const FormField = ({
projectRef,
name,
properties,
formValues,
disabled = false,
setFieldValue,
organizationSlug,
control,
hasAccess,
disabled: disabledProp,
}: FormFieldProps) => {
const [hidden, setHidden] = useState(!!properties.isSecret)
const [dateAsText, setDateAsText] = useState(
formValues[name] ? formatDate(new Date(formValues[name])) : ''
)
const { description: originalDescription } = properties
let description = originalDescription
useEffect(() => {
if (properties.show && properties.show.key && !formValues[properties.show.key]) {
setFieldValue(name, '')
setDateAsText('')
}
}, [properties.show && properties.show.key && !formValues[properties.show.key]])
if (originalDescription && projectRef) {
description = originalDescription.replace(
/\(\.\.\/auth\/(.*?)\)/g,
`(/project/${projectRef}/auth/$1)`
)
}
const fieldValue = useWatch_Shadcn_({ control, name })
if (!hasAccess) {
const planMessage = organizationSlug
? `Only available on [Pro plan](/org/${organizationSlug}/billing?panel=subscriptionPlan) and above.`
: ''
description = originalDescription ? `${originalDescription} ${planMessage}` : planMessage
}
const disabled =
disabledProp || (properties.type === 'boolean' ? !hasAccess && !fieldValue : !hasAccess)
const showValue = useWatch_Shadcn_({
control,
name: properties.show?.key,
disabled: properties.show == null,
})
if (properties.show) {
if (properties.show.matches) {
if (!properties.show.matches.includes(formValues[properties.show.key])) {
if (!properties.show.matches.includes(showValue)) {
return null
}
} else if (!formValues[properties.show.key]) {
} else if (!showValue) {
return null
}
}
@@ -54,241 +88,304 @@ const FormField = ({
switch (properties.type) {
case 'datetime':
return (
<Input
size="small"
layout="vertical"
id={name}
name={name}
type="text"
value={dateAsText}
readOnly
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.description}
</ReactMarkdown>
) : null
}
actions={
<DatePicker
selectsRange={false}
minDate={new Date()}
from={formValues[name]}
to={formValues[name]}
onChange={(date) => {
if (date && date.to) {
setFieldValue(name, date.to)
setDateAsText(formatDate(new Date(date.to)))
} else {
setDateAsText('')
setFieldValue(name, '')
}
}}
>
<span>Pick</span>
</DatePicker>
}
/>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{description}
</ReactMarkdown>
) : null
}
>
<FormControl_Shadcn_>
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="outline"
className="w-full justify-start text-left font-normal px-3 py-4"
icon={<CalendarIcon className="h-4 w-4" />}
size="small"
>
{field.value ? format(new Date(field.value), 'PPP') : 'Pick a date'}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={(date) => {
field.onChange(date?.toISOString())
}}
initialFocus
/>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
</>
)
case 'string':
return (
<Input
size="small"
layout="vertical"
id={name}
name={name}
disabled={disabled}
type={hidden ? 'password' : 'text'}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<Markdown
content={properties.descriptionOptional}
className="text-foreground-lighter"
/>
) : null
}
descriptionText={
properties.description ? (
<Markdown content={properties.description} className="text-foreground-lighter" />
) : null
}
actions={
!!properties.isSecret ? (
<Button
icon={hidden ? <Eye /> : <EyeOff />}
type="default"
onClick={() => setHidden(!hidden)}
/>
) : (
<span className="mr-3 text-foreground-lighter">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
)
}
/>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
description ? (
<Markdown content={description} className="text-foreground-lighter" />
) : null
}
>
<FormControl_Shadcn_ className="col-span-6">
{properties.units ? (
<PrePostTab
postTab={
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
}
className="w-full"
>
<DataInput
{...field}
type={properties.isSecret ? 'password' : 'text'}
id={name}
size="small"
copy
reveal
/>
</PrePostTab>
) : (
<DataInput
{...field}
type={properties.isSecret ? 'password' : 'text'}
id={name}
size="small"
copy
reveal
/>
)}
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
</>
)
case 'multiline-string':
return (
<Input.TextArea
size="small"
layout="vertical"
id={name}
name={name}
disabled={disabled}
type={hidden ? 'password' : 'text'}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : undefined
}
descriptionText={
properties.description ? (
<Markdown content={properties.description} className="text-foreground-lighter" />
) : null
}
actions={
!!properties.isSecret ? (
<Button
icon={hidden ? <Eye /> : <EyeOff />}
type="default"
onClick={() => setHidden(!hidden)}
/>
) : (
<span className="mr-3 text-scale-900">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
)
}
/>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
description ? (
<Markdown content={description} className="text-foreground-lighter" />
) : null
}
>
<FormControl_Shadcn_ className="col-span-6">
<Textarea
{...field}
id={name}
rows={4}
placeholder="Enter multi-line text"
className="resize-none"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
</>
)
case 'number':
return (
<InputNumber
size="small"
layout="vertical"
id={name}
name={name}
disabled={disabled}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<Markdown content={properties.description} className="text-foreground-lighter" />
) : null
}
actions={
<span className="mr-3 text-foreground-lighter">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
}
/>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
description ? (
<Markdown content={description} className="text-foreground-lighter" />
) : null
}
>
<FormControl_Shadcn_ className="col-span-6">
{properties.units ? (
<PrePostTab
postTab={
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
}
className="w-full"
>
<Input_Shadcn_
{...field}
id={name}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</PrePostTab>
) : (
<Input_Shadcn_
{...field}
id={name}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
)}
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
</>
)
case 'boolean':
return (
<Toggle
size="small"
id={name}
name={name}
disabled={disabled}
label={
<div className="flex items-center gap-x-2">
<span>{properties.title}</span>
{properties.link && (
<a href={properties.link} target="_blank" rel="noreferrer noopener">
<InfoTooltip side="bottom">Documentation</InfoTooltip>
</a>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
<div className="flex flex-col gap-1">
{description ? <Markdown content={description} /> : null}
{properties.link && (
<span>
<Button asChild type="default" size="tiny" icon={<ExternalLink />}>
<a href={properties.link} target="_blank" rel="noreferrer noopener">
Documentation
</a>
</Button>
</span>
)}
</div>
}
>
<FormControl_Shadcn_ className="col-span-6">
<Switch
id={name}
checked={field.value}
onCheckedChange={field.onChange}
size="small"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
</div>
}
descriptionText={
properties.description ? <Markdown content={properties.description} /> : null
}
/>
/>
</SheetSection>
<Separator className="w-full" />
</>
)
case 'select':
return (
<Listbox
size="small"
name={name}
disabled={disabled}
label={properties.title}
descriptionText={
properties.description ? (
<ReactMarkdown
unwrapDisallowed
disallowedElements={['p']}
className="form-field-markdown"
>
{properties.description}
</ReactMarkdown>
) : null
}
defaultValue={properties.enum[0]}
>
{properties.enum.map((option: Enum) => {
return (
<Listbox.Option
id={option.value}
key={option.value}
label={option.label}
value={option.value}
addOnBefore={() => {
return option.icon ? (
<img
alt={`${option.label} icon`}
className="h-6 w-6"
src={`${BASE_PATH}/img/icons/${option.icon}`}
/>
) : null
}}
>
{option.label}
</Listbox.Option>
)
})}
</Listbox>
<>
<SheetSection>
<FormField_Shadcn_
control={control}
name={name}
disabled={disabled}
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label={properties.title}
description={
description ? (
<ReactMarkdown
unwrapDisallowed
disallowedElements={['p']}
className="form-field-markdown"
>
{description}
</ReactMarkdown>
) : null
}
>
<FormControl_Shadcn_ className="col-span-6">
<Select_Shadcn_
defaultValue={properties.enum[0]}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an option" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{properties.enum.map((option: Enum) => (
<SelectItem_Shadcn_ key={option.value} value={option.value}>
<span className="flex gap-2 items-center">
{option.icon ? (
<img
alt={`${option.label} icon`}
className="h-6 w-6"
src={`${BASE_PATH}/img/icons/${option.icon}`}
/>
) : null}
{option.label}
</span>
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
</>
)
default:
break
return null
}
return null
}
export default FormField
@@ -1,3 +1,4 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
@@ -6,24 +7,38 @@ import { DocsButton } from 'components/ui/DocsButton'
import { ResourceItem } from 'components/ui/Resource/ResourceItem'
import type { components } from 'data/api'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import { useHasEntitlementAccess } from 'hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useStaticEffectEvent } from 'hooks/useStaticEffectEvent'
import { BASE_PATH } from 'lib/constants'
import { Check } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import { useEffect, useId, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui'
import {
Button,
Form_Shadcn_,
FormControl_Shadcn_,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
import { AuthAlert } from './AuthAlert'
import type { Provider } from './AuthProvidersForm.types'
import FormField from './FormField'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
interface ProviderFormProps {
config: components['schemas']['GoTrueConfigResponse']
@@ -70,31 +85,36 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
const hasEntitlementAccess = useHasEntitlementAccess()
const INITIAL_VALUES = (() => {
const initialValues: { [x: string]: string | boolean } = {}
Object.keys(provider.properties).forEach((key) => {
const isDoubleNegative = doubleNegativeKeys.includes(key)
if (provider.title === 'SAML 2.0') {
const configValue = (config as any)[key]
initialValues[key] =
configValue || (provider.properties[key].type === 'boolean' ? false : '')
} else {
if (isDoubleNegative) {
initialValues[key] = !(config as any)[key]
} else {
const getValuesForProvider = useStaticEffectEvent(
(config: components['schemas']['GoTrueConfigResponse']) => {
const values: { [x: string]: string | boolean } = {}
Object.keys(provider.properties).forEach((key) => {
const isDoubleNegative = doubleNegativeKeys.includes(key)
if (provider.title === 'SAML 2.0') {
const configValue = (config as any)[key]
initialValues[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
values[key] = configValue || (provider.properties[key].type === 'boolean' ? false : '')
} else {
if (isDoubleNegative) {
values[key] = !(config as any)[key]
} else {
const configValue = (config as any)[key]
values[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
}
}
}
})
return initialValues
})()
})
return values
}
)
const onSubmit = (values: any, { resetForm }: any) => {
const INITIAL_VALUES = useMemo(() => {
return getValuesForProvider(config)
}, [config, getValuesForProvider])
const onSubmit = (values: any) => {
const payload = { ...values }
Object.keys(values).map((x: string) => {
if (doubleNegativeKeys.includes(x)) payload[x] = !values[x]
@@ -109,10 +129,10 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onSuccess: () => {
resetForm({ values: { ...values }, initialValues: { ...values } })
onSuccess: (newValues) => {
setOpen(false)
setUrlProvider(null)
form.reset(getValuesForProvider(newValues))
toast.success('Successfully updated settings')
},
}
@@ -133,6 +153,19 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
setOpen(isProviderInQuery)
}, [urlProvider, provider.title])
const form = useForm({
defaultValues: INITIAL_VALUES,
resolver: yupResolver(provider.validationSchema),
shouldUnregister: true,
})
useEffect(() => {
if (open) {
form.reset(INITIAL_VALUES)
}
}, [open, form, INITIAL_VALUES])
const formId = useId()
return (
<>
<ResourceItem
@@ -164,7 +197,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
</ResourceItem>
<Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent className="flex flex-col gap-0">
<SheetContent className="flex flex-col gap-0" size="lg">
<SheetHeader className="shrink-0 flex items-center gap-4">
<img
src={`${BASE_PATH}/img/icons/${provider.misc.iconKey}${provider.misc.hasLightIcon && !resolvedTheme?.includes('dark') ? '-light' : ''}.svg`}
@@ -174,123 +207,99 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
/>
<SheetTitle>{provider.title}</SheetTitle>
</SheetHeader>
<Form
id={`provider-${provider.title}-form`}
name={`provider-${provider.title}-form`}
initialValues={INITIAL_VALUES}
validationSchema={provider.validationSchema}
onSubmit={onSubmit}
className="flex-1 overflow-hidden flex flex-col"
>
{({ handleReset, initialValues, values, setFieldValue }: any) => {
const noChanges = JSON.stringify(initialValues) === JSON.stringify(values)
return (
<>
<div className="flex-1 overflow-y-auto group py-6 px-4 md:px-6 text-foreground">
<div className="mx-auto max-w-lg space-y-6">
<AuthAlert
title={provider.title}
isHookSendSMSEnabled={config.HOOK_SEND_SMS_ENABLED}
<Form_Shadcn_ {...form}>
<form
id={formId}
name={formId}
className="overflow-y-auto flex-grow px-0"
onSubmit={form.handleSubmit(onSubmit)}
>
<AuthAlert
title={provider.title}
isHookSendSMSEnabled={config.HOOK_SEND_SMS_ENABLED}
/>
{Object.keys(provider.properties).map((x: string) => {
const { entitlementKey } = provider.properties[x]
const hasAccess = entitlementKey == null || hasEntitlementAccess(entitlementKey)
return (
<FormField
key={x}
projectRef={projectRef}
organizationSlug={organization?.slug}
name={x}
properties={provider.properties[x]}
control={form.control}
disabled={shouldDisableField(x) || !canUpdateConfig}
hasAccess={hasAccess}
/>
)
})}
{provider?.misc?.alert && (
<SheetSection>
<Admonition
type="warning"
title={provider.misc.alert.title}
description={<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>}
/>
</SheetSection>
)}
{provider.misc.requiresRedirect && (
<SheetSection>
<FormItemLayout
layout="horizontal"
label="Callback URL (for OAuth)"
description={
<Markdown
content={provider.misc.helper}
className="text-foreground-lighter"
/>
{Object.keys(provider.properties).map((x: string) => {
let description = provider.properties[x].description
if (description && projectRef) {
description = description.replace(
/\(\.\.\/auth\/(.*?)\)/g,
`(/project/${projectRef}/auth/$1)`
)
}
const { entitlementKey } = provider.properties[x]
const hasAccess =
entitlementKey == null || hasEntitlementAccess(entitlementKey)
const properties = {
...provider.properties[x],
description: hasAccess
? description
: `${description} Only available on [Pro plan](/org/${organization?.slug}/billing?panel=subscriptionPlan) and above.`,
}
const shouldDisable =
properties.type === 'boolean' ? !hasAccess && !values[x] : !hasAccess
return (
<FormField
key={x}
name={x}
setFieldValue={setFieldValue}
properties={properties}
formValues={values}
disabled={shouldDisableField(x) || !canUpdateConfig || shouldDisable}
/>
)
})}
{provider?.misc?.alert && (
<Admonition
type="warning"
title={provider.misc.alert.title}
description={
<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>
}
/>
)}
{provider.misc.requiresRedirect && (
<Input
copy
readOnly
disabled
label="Callback URL (for OAuth)"
value={`${endpoint}/auth/v1/callback`}
descriptionText={
<Markdown
content={provider.misc.helper}
className="text-foreground-lighter"
/>
}
/>
)}
</div>
</div>
<SheetFooter className="shrink-0">
<div className="flex items-center justify-between w-full">
<DocsButton href={provider.link} />
<div className="flex items-center gap-x-3">
<Button
type="default"
htmlType="reset"
onClick={() => {
handleReset()
setOpen(false)
setUrlProvider(null)
}}
disabled={isUpdatingConfig}
>
Cancel
</Button>
<ButtonTooltip
htmlType="submit"
loading={isUpdatingConfig}
disabled={isUpdatingConfig || !canUpdateConfig || noChanges}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateConfig
? 'You need additional permissions to update provider settings'
: undefined,
},
}}
>
Save
</ButtonTooltip>
</div>
</div>
</SheetFooter>
</>
)
}}
</Form>
}
>
<Input copy readOnly value={endpoint ? `${endpoint}/auth/v1/callback` : ''} />
</FormItemLayout>
</SheetSection>
)}
</form>
</Form_Shadcn_>
<SheetFooter className="shrink-0">
<div className="flex items-center justify-between w-full">
<DocsButton href={provider.link} />
<div className="flex items-center gap-x-3">
<Button
type="default"
htmlType="reset"
onClick={() => {
setOpen(false)
setUrlProvider(null)
form.reset()
}}
disabled={isUpdatingConfig}
>
Cancel
</Button>
<ButtonTooltip
form={formId}
htmlType="submit"
loading={isUpdatingConfig}
disabled={isUpdatingConfig || !canUpdateConfig || !form.formState.isDirty}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateConfig
? 'You need additional permissions to update provider settings'
: undefined,
},
}}
>
Save
</ButtonTooltip>
</div>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
</>
@@ -16,8 +16,8 @@ const PROVIDER_EMAIL = {
link: `${DOCS_URL}/guides/auth/passwords`,
properties: {
EXTERNAL_EMAIL_ENABLED: {
title: 'Enable Email provider',
description: 'This will enable Email based signup and login for your application',
title: 'Enable email provider',
description: 'Allow email-based sign up and log in for your application.',
type: 'boolean',
},
MAILER_SECURE_EMAIL_CHANGE_ENABLED: {
@@ -34,22 +34,20 @@ const PROVIDER_EMAIL = {
},
PASSWORD_HIBP_ENABLED: {
title: 'Prevent use of leaked passwords',
description:
'Rejects the use of known or easy to guess passwords on sign up or password change. Powered by the HaveIBeenPwned.org Pwned Passwords API.',
description: `Rejects the use of known or easy to guess passwords on sign up or password change. Powered by the HaveIBeenPwned.org Pwned Passwords API. [Learn more](${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection)`,
type: 'boolean',
link: `${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection`,
entitlementKey: 'auth.password_hibp',
},
PASSWORD_MIN_LENGTH: {
title: 'Minimum password length',
type: 'number',
description:
'Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.',
'Passwords shorter than this value will be rejected as weak. Minimum 6 characters, though 8 or more is recommended.',
units: 'characters',
},
PASSWORD_REQUIRED_CHARACTERS: {
type: 'select',
title: 'Password Requirements',
title: 'Password requirements',
description: 'Passwords that do not have at least one of each will be rejected as weak.',
enum: [
{
@@ -73,15 +71,15 @@ const PROVIDER_EMAIL = {
},
MAILER_OTP_EXP: {
title: 'Email OTP Expiration',
title: 'Email OTP expiration',
type: 'number',
description: 'Duration before an email otp / link expires.',
description: 'Duration before an email OTP / link expires.',
units: 'seconds',
},
MAILER_OTP_LENGTH: {
title: 'Email OTP Length',
title: 'Email OTP length',
type: 'number',
description: 'Number of digits in the email OTP',
description: 'Number of digits in the email OTP.',
units: 'number',
},
},
@@ -372,7 +372,7 @@ export const FormLayout = React.forwardRef<
>
{hasLabel && isReactForm ? (
<FormLabel_Shadcn_
className="text-foreground flex gap-2 items-center break-all"
className="text-foreground flex gap-2 items-center break-words"
data-formlayout-id="formLabel"
htmlFor={props.name || id}
>
@@ -380,7 +380,7 @@ export const FormLayout = React.forwardRef<
</FormLabel_Shadcn_>
) : (
<Label_Shadcn_
className="text-foreground flex gap-2 items-center break-all leading-normal"
className="text-foreground flex gap-2 items-center break-words leading-normal"
data-formlayout-id="label"
htmlFor={props.name || id}
>