diff --git a/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx new file mode 100644 index 000000000..6205b1341 --- /dev/null +++ b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx @@ -0,0 +1,119 @@ +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Trans } from '@lingui/react/macro'; +import { useState } from 'react'; + +export type BrandingPreferencesResetDialogProps = { + hasAdvancedBranding: boolean; + isSubmitting: boolean; + onReset: () => Promise; + trigger?: React.ReactNode; +}; + +export const BrandingPreferencesResetDialog = ({ + hasAdvancedBranding, + isSubmitting, + onReset, + trigger, +}: BrandingPreferencesResetDialogProps) => { + const [open, setOpen] = useState(false); + const [isResetting, setIsResetting] = useState(false); + + const isLoading = isSubmitting || isResetting; + + const handleResetToDefaults = async () => { + setIsResetting(true); + + try { + await onReset(); + setOpen(false); + } catch { + // The submit handler surfaces its own error toast. Keep the dialog open + // so the user can retry. + } finally { + setIsResetting(false); + } + }; + + return ( + !isLoading && setOpen(value)}> + + {trigger ?? ( + + )} + + + + + + Reset branding preferences + + + + + This will reset all branding preferences to their default values and save the changes immediately. + + + + + + +

+ Once confirmed, the following will be reset: +

+ +
    +
  • + Custom branding enabled setting +
  • +
  • + Branding logo +
  • +
  • + Brand website and brand details +
  • +
  • + Brand colours, including background, foreground, primary, and border colours +
  • + + {hasAdvancedBranding && ( + <> +
  • + Border radius +
  • +
  • + Custom CSS +
  • + + )} +
+
+
+ + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/forms/branding-preferences-form.tsx b/apps/remix/app/components/forms/branding-preferences-form.tsx index ef3ff6b34..e556ff6cf 100644 --- a/apps/remix/app/components/forms/branding-preferences-form.tsx +++ b/apps/remix/app/components/forms/branding-preferences-form.tsx @@ -7,6 +7,7 @@ import { } from '@documenso/lib/constants/branding'; import { DEFAULT_BRAND_COLORS, DEFAULT_BRAND_RADIUS } from '@documenso/lib/constants/theme'; import { ZCssVarsSchema } from '@documenso/lib/types/css-vars'; +import { normalizeBrandingColors } from '@documenso/lib/utils/normalize-branding-colors'; import { cn } from '@documenso/ui/lib/utils'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@documenso/ui/primitives/accordion'; import { Button } from '@documenso/ui/primitives/button'; @@ -23,6 +24,7 @@ import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { BrandingPreferencesResetDialog } from '~/components/dialogs/branding-preferences-reset-dialog'; import { useOptionalCurrentTeam } from '~/providers/team'; import { useCspNonce } from '~/utils/nonce'; @@ -74,6 +76,7 @@ export function BrandingPreferencesForm({ const [previewUrl, setPreviewUrl] = useState(''); const [hasLoadedPreview, setHasLoadedPreview] = useState(false); + const [colorPickerKey, setColorPickerKey] = useState(0); const parsedColors = ZCssVarsSchema.safeParse(settings.brandingColors); const initialColors = parsedColors.success ? parsedColors.data : {}; @@ -96,6 +99,42 @@ export function BrandingPreferencesForm({ const isBrandingEnabled = form.watch('brandingEnabled'); + const hasResetBrandingColors = + settings.brandingColors === null || + settings.brandingColors === undefined || + (parsedColors.success && normalizeBrandingColors(parsedColors.data) === null); + + // Only show the reset action when the saved settings actually differ from the + // defaults, so it never renders as a pointless disabled button. + const isResetToDefaultsVisible = + settings.brandingEnabled !== (canInherit ? null : false) || + !!settings.brandingLogo || + !!settings.brandingUrl || + !!settings.brandingCompanyDetails || + !!settings.brandingCss || + !hasResetBrandingColors; + + const handleResetToDefaults = async () => { + const data: TBrandingPreferencesFormSchema = { + brandingEnabled: canInherit ? null : false, + brandingLogo: null, + brandingUrl: '', + brandingCompanyDetails: '', + brandingColors: {}, + brandingCss: '', + }; + + await onFormSubmit(data); + + if (previewUrl.startsWith('blob:')) { + URL.revokeObjectURL(previewUrl); + } + + setPreviewUrl(''); + setColorPickerKey((key) => key + 1); + form.reset(data); + }; + const getSavedLogoPreviewUrl = () => { if (!settings.brandingLogo) { return ''; @@ -397,6 +436,7 @@ export function BrandingPreferencesForm({ + ) : undefined + } />