fix: API token deletion not reflected in cache until page reload (#1128)

Stops the API token copy card from continuing to appear when a newly created
token has been subsequently deleted.
This commit is contained in:
Chirag Chandrashekhar
2024-07-24 09:01:56 +05:30
committed by GitHub
parent c2374a9d65
commit 994f6867f5
3 changed files with 51 additions and 26 deletions

View File

@ -32,7 +32,7 @@ export default async function ApiTokensPage() {
<hr className="my-4" /> <hr className="my-4" />
<ApiTokenForm className="max-w-xl" /> <ApiTokenForm className="max-w-xl" tokens={tokens} />
<hr className="mb-4 mt-8" /> <hr className="mb-4 mt-8" />

View File

@ -26,7 +26,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
const team = await getTeamByUrl({ userId: user.id, teamUrl }); const team = await getTeamByUrl({ userId: user.id, teamUrl });
let tokens: GetTeamTokensResponse | null = null; let tokens: GetTeamTokensResponse | undefined = undefined;
try { try {
tokens = await getTeamTokens({ userId: user.id, teamId: team.id }); tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
@ -63,7 +63,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<hr className="my-4" /> <hr className="my-4" />
<ApiTokenForm className="max-w-xl" teamId={team.id} /> <ApiTokenForm className="max-w-xl" teamId={team.id} tokens={tokens} />
<hr className="mb-4 mt-8" /> <hr className="mb-4 mt-8" />

View File

@ -1,14 +1,16 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import type { ApiToken } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client'; import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema'; import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
@ -44,23 +46,37 @@ const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>; type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>;
type NewlyCreatedToken = {
id: number;
token: string;
};
export type ApiTokenFormProps = { export type ApiTokenFormProps = {
className?: string; className?: string;
teamId?: number; teamId?: number;
tokens?: Pick<ApiToken, 'id'>[];
}; };
export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => { export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
const router = useRouter(); const router = useRouter();
const [isTransitionPending, startTransition] = useTransition();
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
const { toast } = useToast(); const { toast } = useToast();
const [newlyCreatedToken, setNewlyCreatedToken] = useState(''); const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
const [noExpirationDate, setNoExpirationDate] = useState(false); const [noExpirationDate, setNoExpirationDate] = useState(false);
// This lets us hide the token from being copied if it has been deleted without
// resorting to a useEffect or any other fanciness. This comes at the cost of it
// taking slighly longer to appear since it will need to wait for the router.refresh()
// to finish updating.
const hasNewlyCreatedToken =
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({ const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
onSuccess(data) { onSuccess(data) {
setNewlyCreatedToken(data.token); setNewlyCreatedToken(data);
}, },
}); });
@ -110,7 +126,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
form.reset(); form.reset();
router.refresh(); startTransition(() => router.refresh());
} catch (error) { } catch (error) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') { if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
toast({ toast({
@ -216,7 +232,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
type="submit" type="submit"
className="hidden md:inline-flex" className="hidden md:inline-flex"
disabled={!form.formState.isDirty} disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting} loading={form.formState.isSubmitting || isTransitionPending}
> >
Create token Create token
</Button> </Button>
@ -225,7 +241,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
<Button <Button
type="submit" type="submit"
disabled={!form.formState.isDirty} disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting} loading={form.formState.isSubmitting || isTransitionPending}
> >
Create token Create token
</Button> </Button>
@ -234,24 +250,33 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
</form> </form>
</Form> </Form>
{newlyCreatedToken && ( <AnimatePresence initial={!hasNewlyCreatedToken}>
<Card className="mt-8" gradient> {newlyCreatedToken && hasNewlyCreatedToken && (
<motion.div
className="mt-8"
initial={{ opacity: 0, y: -40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 40 }}
>
<Card gradient>
<CardContent className="p-4"> <CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
Your token was created successfully! Make sure to copy it because you won't be able to Your token was created successfully! Make sure to copy it because you won't be
see it again! able to see it again!
</p> </p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm"> <p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken} {newlyCreatedToken.token}
</p> </p>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken)}> <Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
Copy token Copy token
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
</motion.div>
)} )}
</AnimatePresence>
</div> </div>
); );
}; };