mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 02:15:05 +10:00
feat: add branding preferences reset dialog (#3032)
This commit is contained in:
@@ -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<void>;
|
||||||
|
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 (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isLoading && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger ?? (
|
||||||
|
<Button variant="destructive" type="button" size="sm" 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';
|
} from '@documenso/lib/constants/branding';
|
||||||
import { DEFAULT_BRAND_COLORS, DEFAULT_BRAND_RADIUS } from '@documenso/lib/constants/theme';
|
import { DEFAULT_BRAND_COLORS, DEFAULT_BRAND_RADIUS } from '@documenso/lib/constants/theme';
|
||||||
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
|
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 { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@documenso/ui/primitives/accordion';
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@documenso/ui/primitives/accordion';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -23,6 +24,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { BrandingPreferencesResetDialog } from '~/components/dialogs/branding-preferences-reset-dialog';
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
import { useCspNonce } from '~/utils/nonce';
|
import { useCspNonce } from '~/utils/nonce';
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ export function BrandingPreferencesForm({
|
|||||||
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||||
const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
|
const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
|
||||||
|
const [colorPickerKey, setColorPickerKey] = useState(0);
|
||||||
|
|
||||||
const parsedColors = ZCssVarsSchema.safeParse(settings.brandingColors);
|
const parsedColors = ZCssVarsSchema.safeParse(settings.brandingColors);
|
||||||
const initialColors = parsedColors.success ? parsedColors.data : {};
|
const initialColors = parsedColors.success ? parsedColors.data : {};
|
||||||
@@ -96,6 +99,42 @@ export function BrandingPreferencesForm({
|
|||||||
|
|
||||||
const isBrandingEnabled = form.watch('brandingEnabled');
|
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 = () => {
|
const getSavedLogoPreviewUrl = () => {
|
||||||
if (!settings.brandingLogo) {
|
if (!settings.brandingLogo) {
|
||||||
return '';
|
return '';
|
||||||
@@ -397,6 +436,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`background-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.background}
|
defaultValue={DEFAULT_BRAND_COLORS.background}
|
||||||
@@ -420,6 +460,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`foreground-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.foreground}
|
defaultValue={DEFAULT_BRAND_COLORS.foreground}
|
||||||
@@ -443,6 +484,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`primary-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.primary}
|
defaultValue={DEFAULT_BRAND_COLORS.primary}
|
||||||
@@ -466,6 +508,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`primary-foreground-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.primaryForeground}
|
defaultValue={DEFAULT_BRAND_COLORS.primaryForeground}
|
||||||
@@ -489,6 +532,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`border-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.border}
|
defaultValue={DEFAULT_BRAND_COLORS.border}
|
||||||
@@ -512,6 +556,7 @@ export function BrandingPreferencesForm({
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
|
key={`ring-${colorPickerKey}`}
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
value={field.value ?? ''}
|
value={field.value ?? ''}
|
||||||
defaultValue={DEFAULT_BRAND_COLORS.ring}
|
defaultValue={DEFAULT_BRAND_COLORS.ring}
|
||||||
@@ -593,6 +638,15 @@ export function BrandingPreferencesForm({
|
|||||||
isDirty={hasUnsavedChanges}
|
isDirty={hasUnsavedChanges}
|
||||||
isSubmitting={form.formState.isSubmitting}
|
isSubmitting={form.formState.isSubmitting}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
|
resetToDefaults={
|
||||||
|
isResetToDefaultsVisible ? (
|
||||||
|
<BrandingPreferencesResetDialog
|
||||||
|
hasAdvancedBranding={hasAdvancedBranding}
|
||||||
|
isSubmitting={form.formState.isSubmitting}
|
||||||
|
onReset={handleResetToDefaults}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user