feat: add offline development support (#987)

## Description

Add support to develop without network access since TRPC by default will
prevent network requests when offline.

https://tanstack.com/query/v4/docs/framework/react/guides/network-mode#network-mode-always

## Changes Made

- Add dynamic logic to toggle offline development
- Removed teams feature flag
This commit is contained in:
David Nguyen
2024-03-05 22:06:48 +11:00
committed by GitHub
parent f6f9c301da
commit 70494fa5bb
6 changed files with 46 additions and 243 deletions

View File

@ -7,7 +7,6 @@ import { useParams } from 'next/navigation';
import { MenuIcon, SearchIcon } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getRootHref } from '@documenso/lib/utils/params';
import type { User } from '@documenso/prisma/client';
@ -19,7 +18,6 @@ import { CommandMenu } from '../common/command-menu';
import { DesktopNav } from './desktop-nav';
import { MenuSwitcher } from './menu-switcher';
import { MobileNavigation } from './mobile-navigation';
import { ProfileDropdown } from './profile-dropdown';
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
user: User;
@ -29,10 +27,6 @@ export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
const params = useParams();
const { getFlag } = useFeatureFlags();
const isTeamsEnabled = getFlag('app_teams');
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
@ -47,34 +41,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
return () => window.removeEventListener('scroll', onScroll);
}, []);
if (!isTeamsEnabled) {
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[60] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
className,
)}
{...props}
>
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
<Link
href="/"
className="focus-visible:ring-ring ring-offset-background rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Logo className="h-6 w-auto" />
</Link>
<DesktopNav />
<div className="flex gap-x-4 md:ml-8">
<ProfileDropdown user={user} />
</div>
</div>
</header>
);
}
return (
<header
className={cn(

View File

@ -1,177 +0,0 @@
'use client';
import Link from 'next/link';
import {
Braces,
CreditCard,
FileSpreadsheet,
Lock,
LogOut,
User as LucideUser,
Monitor,
Moon,
Palette,
Sun,
UserCog,
} from 'lucide-react';
import { signOut } from 'next-auth/react';
import { useTheme } from 'next-themes';
import { LuGithub } from 'react-icons/lu';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
export type ProfileDropdownProps = {
user: User;
};
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { getFlag } = useFeatureFlags();
const { theme, setTheme } = useTheme();
const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing');
const avatarFallback = user.name
? extractInitials(user.name)
: user.email.slice(0, 1).toUpperCase();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
title="Profile Dropdown"
className="relative h-10 w-10 rounded-full"
>
<Avatar className="h-10 w-10">
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="z-[60] w-56" align="end" forceMount>
<DropdownMenuLabel>Account</DropdownMenuLabel>
{isUserAdmin && (
<>
<DropdownMenuItem asChild>
<Link href="/admin" className="cursor-pointer">
<UserCog className="mr-2 h-4 w-4" />
Admin
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="cursor-pointer">
<LucideUser className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings/security" className="cursor-pointer">
<Lock className="mr-2 h-4 w-4" />
Security
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/settings/tokens" className="cursor-pointer">
<Braces className="mr-2 h-4 w-4" />
API Tokens
</Link>
</DropdownMenuItem>
{isBillingEnabled && (
<DropdownMenuItem asChild>
<Link href="/settings/billing" className="cursor-pointer">
<CreditCard className="mr-2 h-4 w-4" />
Billing
</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/templates" className="cursor-pointer">
<FileSpreadsheet className="mr-2 h-4 w-4" />
Templates
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="mr-2 h-4 w-4" />
Themes
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="z-[60]">
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">
<Sun className="mr-2 h-4 w-4" /> Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Moon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
<Monitor className="mr-2 h-4 w-4" />
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="https://github.com/documenso/documenso"
className="cursor-pointer"
target="_blank"
>
<LuGithub className="mr-2 h-4 w-4" />
Star on Github
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() =>
void signOut({
callbackUrl: '/',
})
}
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -19,7 +19,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
const { getFlag } = useFeatureFlags();
const isBillingEnabled = getFlag('app_billing');
const isTeamsEnabled = getFlag('app_teams');
return (
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
@ -36,20 +35,18 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
</Button>
</Link>
{isTeamsEnabled && (
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
)}
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
<Link href="/settings/security">
<Button

View File

@ -19,7 +19,6 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
const { getFlag } = useFeatureFlags();
const isBillingEnabled = getFlag('app_billing');
const isTeamsEnabled = getFlag('app_teams');
return (
<div
@ -39,20 +38,18 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
</Button>
</Link>
{isTeamsEnabled && (
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
)}
<Link href="/settings/teams">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/teams') && 'bg-secondary',
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
</Button>
</Link>
<Link href="/settings/security">
<Button

View File

@ -22,7 +22,6 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
*/
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_teams: true,
app_document_page_view_history_sheet: false,
marketing_header_single_player_mode: false,
marketing_profiles_announcement_bar: true,

View File

@ -2,6 +2,7 @@
import { useState } from 'react';
import type { QueryClientConfig } from '@tanstack/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
@ -27,7 +28,27 @@ export interface TrpcProviderProps {
}
export function TrpcProvider({ children }: TrpcProviderProps) {
const [queryClient] = useState(() => new QueryClient());
let queryClientConfig: QueryClientConfig | undefined;
const isDevelopingOffline =
typeof window !== 'undefined' &&
window.location.hostname === 'localhost' &&
!window.navigator.onLine;
if (isDevelopingOffline) {
queryClientConfig = {
defaultOptions: {
queries: {
networkMode: 'always',
},
mutations: {
networkMode: 'always',
},
},
};
}
const [queryClient] = useState(() => new QueryClient(queryClientConfig));
const [trpcClient] = useState(() =>
trpc.createClient({