diff --git a/apps/remix/app/components/(teams)/forms/update-team-form.tsx b/apps/remix/app/components/(teams)/forms/update-team-form.tsx
index 839b52f67..c11b25bc2 100644
--- a/apps/remix/app/components/(teams)/forms/update-team-form.tsx
+++ b/apps/remix/app/components/(teams)/forms/update-team-form.tsx
@@ -3,6 +3,7 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router';
import type { z } from 'zod';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
diff --git a/apps/remix/app/components/(teams)/tables/teams-member-page-data-table.tsx b/apps/remix/app/components/(teams)/tables/teams-member-page-data-table.tsx
deleted file mode 100644
index 2aebb4579..000000000
--- a/apps/remix/app/components/(teams)/tables/teams-member-page-data-table.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type { TeamMemberRole } from '@prisma/client';
-import { Link, useLocation, useNavigate, useSearchParams } from 'react-router';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { Input } from '@documenso/ui/primitives/input';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-
-import { TeamMemberInvitesDataTable } from '~/components/(teams)/tables/team-member-invites-data-table';
-import { TeamMembersDataTable } from '~/components/(teams)/tables/team-members-data-table';
-
-export type TeamsMemberPageDataTableProps = {
- currentUserTeamRole: TeamMemberRole;
- teamId: number;
- teamName: string;
- teamOwnerUserId: number;
-};
-
-export const TeamsMemberPageDataTable = ({
- currentUserTeamRole,
- teamId,
- teamName,
- teamOwnerUserId,
-}: TeamsMemberPageDataTableProps) => {
- const { _ } = useLingui();
-
- const [searchParams] = useSearchParams();
- const navigate = useNavigate();
- const { pathname } = useLocation();
-
- const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
-
- const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
-
- const currentTab = searchParams?.get('tab') === 'invites' ? 'invites' : 'members';
-
- /**
- * Handle debouncing the search query.
- */
- useEffect(() => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('query', debouncedSearchQuery);
-
- if (debouncedSearchQuery === '') {
- params.delete('query');
- }
-
- void navigate(`${pathname}?${params.toString()}`);
- }, [debouncedSearchQuery, pathname, navigate, searchParams]);
-
- return (
-
-
- setSearchQuery(e.target.value)}
- placeholder={_(msg`Search`)}
- />
-
-
-
-
-
- Active
-
-
-
-
-
- Pending
-
-
-
-
-
-
- {currentTab === 'invites' ? (
-
- ) : (
-
- )}
-
- );
-};
diff --git a/apps/remix/app/components/dialogs/team-delete-dialog.tsx b/apps/remix/app/components/dialogs/team-delete-dialog.tsx
index c0c665823..a91e32302 100644
--- a/apps/remix/app/components/dialogs/team-delete-dialog.tsx
+++ b/apps/remix/app/components/dialogs/team-delete-dialog.tsx
@@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router';
import { z } from 'zod';
import { AppError } from '@documenso/lib/errors/app-error';
diff --git a/apps/remix/app/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/remix/app/components/dialogs/webhook-create-dialog.tsx
similarity index 96%
rename from apps/remix/app/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
rename to apps/remix/app/components/dialogs/webhook-create-dialog.tsx
index 724b6adbb..c2c95bca0 100644
--- a/apps/remix/app/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
+++ b/apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@@ -35,17 +35,17 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
-import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
+import { WebhookMultiSelectCombobox } from '../general/webhook-multiselect-combobox';
const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
type TCreateWebhookFormSchema = z.infer;
-export type CreateWebhookDialogProps = {
+export type WebhookCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit;
-export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
+export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@@ -185,7 +185,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
Triggers
- {
onChange(values);
diff --git a/apps/remix/app/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx b/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
similarity index 97%
rename from apps/remix/app/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
rename to apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
index 82cc61c22..d38b0a2b6 100644
--- a/apps/remix/app/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
+++ b/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
@@ -33,13 +33,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
-export type DeleteWebhookDialogProps = {
+export type WebhookDeleteDialogProps = {
webhook: Pick;
onDelete?: () => void;
children: React.ReactNode;
};
-export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
+export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
diff --git a/apps/remix/app/components/forms/team-branding-preferences-form.tsx b/apps/remix/app/components/forms/team-branding-preferences-form.tsx
new file mode 100644
index 000000000..3f937a0b8
--- /dev/null
+++ b/apps/remix/app/components/forms/team-branding-preferences-form.tsx
@@ -0,0 +1,319 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Trans, msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+import { Loader } from 'lucide-react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { getFile } from '@documenso/lib/universal/upload/get-file';
+import { putFile } from '@documenso/lib/universal/upload/put-file';
+import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
+import { trpc } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { Textarea } from '@documenso/ui/primitives/textarea';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
+
+const ZTeamBrandingPreferencesFormSchema = z.object({
+ brandingEnabled: z.boolean(),
+ brandingLogo: z
+ .instanceof(File)
+ .refine((file) => file.size <= MAX_FILE_SIZE, 'File size must be less than 5MB')
+ .refine(
+ (file) => ACCEPTED_FILE_TYPES.includes(file.type),
+ 'Only .jpg, .png, and .webp files are accepted',
+ )
+ .nullish(),
+ brandingUrl: z.string().url().optional().or(z.literal('')),
+ brandingCompanyDetails: z.string().max(500).optional(),
+});
+
+type TTeamBrandingPreferencesFormSchema = z.infer;
+
+export type TeamBrandingPreferencesFormProps = {
+ team: Team;
+ settings?: TeamGlobalSettings | null;
+};
+
+export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPreferencesFormProps) {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+
+ const [previewUrl, setPreviewUrl] = useState('');
+ const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
+
+ const { mutateAsync: updateTeamBrandingSettings } =
+ trpc.team.updateTeamBrandingSettings.useMutation();
+
+ const form = useForm({
+ defaultValues: {
+ brandingEnabled: settings?.brandingEnabled ?? false,
+ brandingUrl: settings?.brandingUrl ?? '',
+ brandingLogo: undefined,
+ brandingCompanyDetails: settings?.brandingCompanyDetails ?? '',
+ },
+ resolver: zodResolver(ZTeamBrandingPreferencesFormSchema),
+ });
+
+ const isBrandingEnabled = form.watch('brandingEnabled');
+
+ const onSubmit = async (data: TTeamBrandingPreferencesFormSchema) => {
+ try {
+ const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = data;
+
+ let uploadedBrandingLogo = settings?.brandingLogo;
+
+ if (brandingLogo) {
+ uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
+ }
+
+ if (brandingLogo === null) {
+ uploadedBrandingLogo = '';
+ }
+
+ await updateTeamBrandingSettings({
+ teamId: team.id,
+ settings: {
+ brandingEnabled,
+ brandingLogo: uploadedBrandingLogo,
+ brandingUrl,
+ brandingCompanyDetails,
+ },
+ });
+
+ toast({
+ title: _(msg`Branding preferences updated`),
+ description: _(msg`Your branding preferences have been updated`),
+ });
+ } catch (err) {
+ toast({
+ title: _(msg`Something went wrong`),
+ description: _(
+ msg`We were unable to update your branding preferences at this time, please try again later`,
+ ),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (settings?.brandingLogo) {
+ const file = JSON.parse(settings.brandingLogo);
+
+ if ('type' in file && 'data' in file) {
+ void getFile(file).then((binaryData) => {
+ const objectUrl = URL.createObjectURL(new Blob([binaryData]));
+
+ setPreviewUrl(objectUrl);
+ setHasLoadedPreview(true);
+ });
+
+ return;
+ }
+ }
+
+ setHasLoadedPreview(true);
+ }, [settings?.brandingLogo]);
+
+ // Cleanup ObjectURL on unmount or when previewUrl changes
+ useEffect(() => {
+ return () => {
+ if (previewUrl.startsWith('blob:')) {
+ URL.revokeObjectURL(previewUrl);
+ }
+ };
+ }, [previewUrl]);
+
+ return (
+
+
+ );
+}
diff --git a/apps/remix/app/components/forms/team-document-preferences-form.tsx b/apps/remix/app/components/forms/team-document-preferences-form.tsx
new file mode 100644
index 000000000..ed7875ab0
--- /dev/null
+++ b/apps/remix/app/components/forms/team-document-preferences-form.tsx
@@ -0,0 +1,312 @@
+'use client';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Trans, msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+import { useSession } from 'next-auth/react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import {
+ SUPPORTED_LANGUAGES,
+ SUPPORTED_LANGUAGE_CODES,
+ isValidLanguageCode,
+} from '@documenso/lib/constants/i18n';
+import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
+import { DocumentVisibility } from '@documenso/prisma/client';
+import { trpc } from '@documenso/trpc/react';
+import { Alert } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+} from '@documenso/ui/primitives/form/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@documenso/ui/primitives/select';
+import { Switch } from '@documenso/ui/primitives/switch';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+const ZTeamDocumentPreferencesFormSchema = z.object({
+ documentVisibility: z.nativeEnum(DocumentVisibility),
+ documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
+ includeSenderDetails: z.boolean(),
+ typedSignatureEnabled: z.boolean(),
+ includeSigningCertificate: z.boolean(),
+});
+
+type TTeamDocumentPreferencesFormSchema = z.infer;
+
+export type TeamDocumentPreferencesFormProps = {
+ team: Team;
+ settings?: TeamGlobalSettings | null;
+};
+
+export const TeamDocumentPreferencesForm = ({
+ team,
+ settings,
+}: TeamDocumentPreferencesFormProps) => {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+ const { data } = useSession();
+
+ const placeholderEmail = data?.user.email ?? 'user@example.com';
+
+ const { mutateAsync: updateTeamDocumentPreferences } =
+ trpc.team.updateTeamDocumentSettings.useMutation();
+
+ const form = useForm({
+ defaultValues: {
+ documentVisibility: settings?.documentVisibility ?? 'EVERYONE',
+ documentLanguage: isValidLanguageCode(settings?.documentLanguage)
+ ? settings?.documentLanguage
+ : 'en',
+ includeSenderDetails: settings?.includeSenderDetails ?? false,
+ typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
+ includeSigningCertificate: settings?.includeSigningCertificate ?? true,
+ },
+ resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
+ });
+
+ const includeSenderDetails = form.watch('includeSenderDetails');
+
+ const onSubmit = async (data: TTeamDocumentPreferencesFormSchema) => {
+ try {
+ const {
+ documentVisibility,
+ documentLanguage,
+ includeSenderDetails,
+ includeSigningCertificate,
+ typedSignatureEnabled,
+ } = data;
+
+ await updateTeamDocumentPreferences({
+ teamId: team.id,
+ settings: {
+ documentVisibility,
+ documentLanguage,
+ includeSenderDetails,
+ typedSignatureEnabled,
+ includeSigningCertificate,
+ },
+ });
+
+ toast({
+ title: _(msg`Document preferences updated`),
+ description: _(msg`Your document preferences have been updated`),
+ });
+ } catch (err) {
+ toast({
+ title: _(msg`Something went wrong!`),
+ description: _(
+ msg`We were unable to update your document preferences at this time, please try again later`,
+ ),
+ });
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/apps/remix/app/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx b/apps/remix/app/components/general/webhook-multiselect-combobox.tsx
similarity index 95%
rename from apps/remix/app/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
rename to apps/remix/app/components/general/webhook-multiselect-combobox.tsx
index 43c883ad1..ba6eb1ac9 100644
--- a/apps/remix/app/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
+++ b/apps/remix/app/components/general/webhook-multiselect-combobox.tsx
@@ -18,15 +18,15 @@ import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitive
import { truncateTitle } from '~/helpers/truncate-title';
-type TriggerMultiSelectComboboxProps = {
+type WebhookMultiSelectComboboxProps = {
listValues: string[];
onChange: (_values: string[]) => void;
};
-export const TriggerMultiSelectCombobox = ({
+export const WebhookMultiSelectCombobox = ({
listValues,
onChange,
-}: TriggerMultiSelectComboboxProps) => {
+}: WebhookMultiSelectComboboxProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedValues, setSelectedValues] = useState([]);
diff --git a/apps/remix/app/components/(teams)/team-billing-portal-button.tsx b/apps/remix/app/components/pages/teams/team-billing-portal-button.tsx
similarity index 100%
rename from apps/remix/app/components/(teams)/team-billing-portal-button.tsx
rename to apps/remix/app/components/pages/teams/team-billing-portal-button.tsx
diff --git a/apps/remix/app/components/pages/teams/team-email-dropdown.tsx b/apps/remix/app/components/pages/teams/team-email-dropdown.tsx
new file mode 100644
index 000000000..d40208dc9
--- /dev/null
+++ b/apps/remix/app/components/pages/teams/team-email-dropdown.tsx
@@ -0,0 +1,96 @@
+'use client';
+
+import { Trans, msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+import { Edit, Loader, Mail, MoreHorizontal, X } from 'lucide-react';
+
+import type { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
+import { trpc } from '@documenso/trpc/react';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@documenso/ui/primitives/dropdown-menu';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { TeamEmailDeleteDialog } from '~/components/dialogs/team-email-delete-dialog';
+import { TeamEmailUpdateDialog } from '~/components/dialogs/team-email-update-dialog';
+
+export type TeamEmailDropdownProps = {
+ team: Awaited>;
+};
+
+export const TeamEmailDropdown = ({ team }: TeamEmailDropdownProps) => {
+ const { _ } = useLingui();
+ const { toast } = useToast();
+
+ const { mutateAsync: resendEmailVerification, isPending: isResendingEmailVerification } =
+ trpc.team.resendTeamEmailVerification.useMutation({
+ onSuccess: () => {
+ toast({
+ title: _(msg`Success`),
+ description: _(msg`Email verification has been resent`),
+ duration: 5000,
+ });
+ },
+ onError: () => {
+ toast({
+ title: _(msg`Something went wrong`),
+ description: _(msg`Unable to resend verification at this time. Please try again.`),
+ variant: 'destructive',
+ duration: 10000,
+ });
+ },
+ });
+
+ return (
+
+
+
+
+
+
+ {!team.teamEmail && team.emailVerification && (
+ {
+ e.preventDefault();
+ void resendEmailVerification({ teamId: team.id });
+ }}
+ >
+ {isResendingEmailVerification ? (
+
+ ) : (
+
+ )}
+ Resend verification
+
+ )}
+
+ {team.teamEmail && (
+ e.preventDefault()}>
+
+ Edit
+
+ }
+ />
+ )}
+
+ e.preventDefault()}>
+
+ Remove
+
+ }
+ />
+
+
+ );
+};
diff --git a/apps/remix/app/components/(teams)/settings/layout/desktop-nav.tsx b/apps/remix/app/components/pages/teams/team-settings-desktop-nav.tsx
similarity index 80%
rename from apps/remix/app/components/(teams)/settings/layout/desktop-nav.tsx
rename to apps/remix/app/components/pages/teams/team-settings-desktop-nav.tsx
index d84b51a3e..321ffafeb 100644
--- a/apps/remix/app/components/(teams)/settings/layout/desktop-nav.tsx
+++ b/apps/remix/app/components/pages/teams/team-settings-desktop-nav.tsx
@@ -4,21 +4,16 @@ import { Trans } from '@lingui/macro';
import { Braces, CreditCard, Globe2Icon, Settings, Settings2, Users, Webhook } from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router';
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
-export type DesktopNavProps = HTMLAttributes;
+export type TeamSettingsDesktopNavProps = HTMLAttributes;
-export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
+export const TeamSettingsDesktopNav = ({ className, ...props }: TeamSettingsDesktopNavProps) => {
const { pathname } = useLocation();
const params = useParams();
- const { getFlag } = useFeatureFlags();
-
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
const teamUrl = typeof params?.teamUrl === 'string' ? params?.teamUrl : '';
const settingsPath = `/t/${teamUrl}/settings`;
@@ -55,20 +50,18 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- {isPublicProfileEnabled && (
-
-
-
- )}
+
+
+
- {isPublicProfileEnabled && (
-
-
-
- )}
+
+
+