mirror of
https://github.com/documenso/documenso.git
synced 2026-07-14 23:07:13 +10:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ff571a9fa | |||
| a3365b9c2d | |||
| 162f246228 | |||
| c5504dd5f3 | |||
| 561a5e3946 | |||
| ff8eebf26c | |||
| 3529999caa | |||
| 23ad742ba4 |
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user