Compare commits

...

8 Commits

Author SHA1 Message Date
Catalin Pit 0ff571a9fa Merge branch 'main' into feat/reset-branding-to-default 2026-07-08 10:33:33 +03:00
Catalin Pit a3365b9c2d chore: undo unrelated changes 2026-07-07 13:00:50 +03:00
Catalin Pit 162f246228 chore: merge main 2026-07-07 12:44:42 +03:00
Catalin Pit c5504dd5f3 Merge branch 'main' into feat/reset-branding-to-default 2026-06-29 10:09:07 +03:00
Catalin Pit 561a5e3946 Merge branch 'main' into feat/reset-branding-to-default 2026-06-26 14:05:54 +03:00
Catalin Pit ff8eebf26c feat: update branding preferences reset dialog with disabled state integration 2026-06-25 14:41:52 +03:00
Catalin Pit 3529999caa Merge branch 'main' into feat/reset-branding-to-default 2026-06-25 12:18:59 +03:00
Catalin Pit 23ad742ba4 feat: add branding preferences reset dialog and integrate with form 2026-06-25 12:18:14 +03:00
5 changed files with 181 additions and 4 deletions
@@ -0,0 +1,118 @@
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 = {
disabled?: boolean;
hasAdvancedBranding: boolean;
isSubmitting: boolean;
onReset: () => Promise<void>;
trigger?: React.ReactNode;
};
export const BrandingPreferencesResetDialog = ({
disabled = false,
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);
} finally {
setIsResetting(false);
}
};
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && !disabled && setOpen(value)}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive" type="button" disabled={disabled || isLoading}>
<Trans>Reset to defaults</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Reset branding preferences</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
This will reset all branding preferences to their default values and save the changes immediately.
</Trans>
</DialogDescription>
</DialogHeader>
<Alert variant="warning">
<AlertDescription>
<p>
<Trans>Once confirmed, the following will be reset:</Trans>
</p>
<ul className="mt-0.5 list-inside list-disc">
<li>
<Trans>Custom branding enabled setting</Trans>
</li>
<li>
<Trans>Branding logo</Trans>
</li>
<li>
<Trans>Brand website and brand details</Trans>
</li>
<li>
<Trans>Brand colours, including background, foreground, primary, and border colours</Trans>
</li>
{hasAdvancedBranding && (
<>
<li>
<Trans>Border radius</Trans>
</li>
<li>
<Trans>Custom CSS</Trans>
</li>
</>
)}
</ul>
</AlertDescription>
</Alert>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary" disabled={isLoading}>
<Trans>Cancel</Trans>
</Button>
</DialogClose>
<Button type="button" variant="destructive" loading={isLoading} onClick={() => void handleResetToDefaults()}>
<Trans>Reset to defaults</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -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<string>('');
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,41 @@ export function BrandingPreferencesForm({
const isBrandingEnabled = form.watch('brandingEnabled');
const hasResetBrandingColors =
settings.brandingColors === null ||
settings.brandingColors === undefined ||
(parsedColors.success && normalizeBrandingColors(parsedColors.data) === null);
const isResetDisabled =
!form.formState.isDirty &&
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 +435,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`background-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.background}
@@ -420,6 +459,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`foreground-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.foreground}
@@ -443,6 +483,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`primary-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.primary}
@@ -466,6 +507,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`primary-foreground-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.primaryForeground}
@@ -489,6 +531,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`border-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.border}
@@ -512,6 +555,7 @@ export function BrandingPreferencesForm({
</FormDescription>
<FormControl>
<ColorPicker
key={`ring-${colorPickerKey}`}
nonce={nonce}
value={field.value ?? ''}
defaultValue={DEFAULT_BRAND_COLORS.ring}
@@ -593,6 +637,14 @@ export function BrandingPreferencesForm({
isDirty={hasUnsavedChanges}
isSubmitting={form.formState.isSubmitting}
onReset={handleReset}
resetToDefaults={
<BrandingPreferencesResetDialog
disabled={isResetDisabled}
hasAdvancedBranding={hasAdvancedBranding}
isSubmitting={form.formState.isSubmitting}
onReset={handleResetToDefaults}
/>
}
/>
</fieldset>
</form>
@@ -3,12 +3,17 @@ import { Button } from '@documenso/ui/primitives/button';
import { Trans, useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { AlertTriangleIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { type ReactNode, useEffect, useRef, useState } from 'react';
export type FormStickySaveBarProps = {
isDirty: boolean;
isSubmitting: boolean;
onReset: () => void;
/**
* Slot for a "reset to defaults" action, rendered after the Save button. Only shown
* while the form is unchanged so it never competes with the Undo/unsaved-changes UI.
*/
resetToDefaults?: ReactNode;
};
/**
@@ -24,7 +29,7 @@ export type FormStickySaveBarProps = {
* shared-layout morph). A 1px sentinel below it detects the stuck state so we can toggle
* the pill chrome.
*/
export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset }: FormStickySaveBarProps) => {
export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset, resetToDefaults }: FormStickySaveBarProps) => {
const { t } = useLingui();
const sentinelRef = useRef<HTMLDivElement>(null);
@@ -109,6 +114,8 @@ export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset }: FormSticky
<Button type="submit" className="shrink-0" size="sm" loading={isSubmitting} disabled={!isDirty}>
<Trans>Save changes</Trans>
</Button>
{!isDirty && resetToDefaults}
</div>
</div>
@@ -88,7 +88,7 @@ export default function OrganisationSettingsDocumentPage() {
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
delegateDocumentOwnership: delegateDocumentOwnership,
delegateDocumentOwnership,
aiFeaturesEnabled,
envelopeExpirationPeriod: envelopeExpirationPeriod ?? undefined,
reminderSettings: reminderSettings ?? undefined,
@@ -82,7 +82,7 @@ export default function TeamsSettingsPage() {
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
}),
delegateDocumentOwnership: delegateDocumentOwnership,
delegateDocumentOwnership,
},
});