Skip to content

Commit

Permalink
Safe actions
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Dec 28, 2023
1 parent 3a96b5b commit 42bef26
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 154 deletions.
2 changes: 1 addition & 1 deletion apps/dashboard/src/actions/connect-bank-account-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const connectBankAccountAction = action(
const { data } = await createBankAccounts(supabase, accounts);

const promisses = data?.map(async (account) => {
// Fetch transactions
// Fetch transactions for each account
const { transactions } = await getTransactions(account.account_id);

// Update bank account last_accessed
Expand Down
72 changes: 0 additions & 72 deletions apps/dashboard/src/actions/index.ts

This file was deleted.

15 changes: 15 additions & 0 deletions apps/dashboard/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,18 @@ const bankAccount = z.object({
});

export const connectBankAccountSchema = z.array(bankAccount);

export const sendFeedbackSchema = z.object({
feedback: z.string(),
});

export const updateTransactionSchema = z.object({
id: z.string(),
note: z.string().optional(),
category: z.string().optional(),
assigned_id: z.string().optional(),
});

export const updateSimilarTransactionsSchema = z.object({
id: z.string(),
});
37 changes: 37 additions & 0 deletions apps/dashboard/src/actions/send-feedback-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use server";

import { env } from "@/env.mjs";
import { createClient } from "@midday/supabase/server";
import { action } from "./safe-action";
import { sendFeedbackSchema } from "./schema";

const baseUrl = "https://api.resend.com";

export const sendFeebackAction = action(
sendFeedbackSchema,
async ({ feedback }) => {
const supabase = createClient();
const {
data: { session },
} = await supabase.auth.getSession();

const res = await fetch(`${baseUrl}/email`, {
method: "POST",
cache: "no-cache",
headers: {
Authorization: `Bearer ${env.RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "feedback@midday.ai",
to: "pontus@lostisland.co",
subject: "Feedback",
text: `${feedback} \nName: ${session?.user?.user_metadata?.name} \nEmail: ${session?.user?.email}`,
}),
});

const json = await res.json();

return json;
}
);
23 changes: 23 additions & 0 deletions apps/dashboard/src/actions/update-similar-transactions-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import { updateSimilarTransactions } from "@midday/supabase/mutations";
import { createClient } from "@midday/supabase/server";
import { revalidateTag } from "next/cache";
import { action } from "./safe-action";
import { updateSimilarTransactionsSchema } from "./schema";

export const updateSimilarTransactionsAction = action(
updateSimilarTransactionsSchema,
async ({ id }) => {
const supabase = createClient();
const user = await getUser();
const teamId = user.data.team_id;

await updateSimilarTransactions(supabase, id);

revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);
revalidateTag(`metrics_${teamId}`);
}
);
33 changes: 33 additions & 0 deletions apps/dashboard/src/actions/update-transaction-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use server";

import { getUser } from "@midday/supabase/cached-queries";
import {
createEnrichmentTransaction,
updateTransaction,
} from "@midday/supabase/mutations";
import { createClient } from "@midday/supabase/server";
import { revalidateTag } from "next/cache";
import { action } from "./safe-action";
import { updateTransactionSchema } from "./schema";

export const updateTransactionAction = action(
updateTransactionSchema,
async ({ id, ...payload }) => {
const supabase = createClient();
const user = await getUser();
const teamId = user.data.team_id;
const { data } = await updateTransaction(supabase, id, payload);

// Add category to global enrichment_transactions
if (data?.category) {
createEnrichmentTransaction(supabase, {
name: data.name,
category: data.category,
});
}

revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);
revalidateTag(`metrics_${teamId}`);
}
);
26 changes: 15 additions & 11 deletions apps/dashboard/src/components/assign-user.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { updateTransactionAction } from "@/actions";
"use client";

import { updateTransactionAction } from "@/actions/update-transaction-action";
import { createClient } from "@midday/supabase/client";
import {
getCurrentUserTeamQuery,
Expand All @@ -13,22 +15,16 @@ import {
SelectValue,
} from "@midday/ui/select";
import { Skeleton } from "@midday/ui/skeleton";
import { startTransition, useEffect, useState } from "react";
import { useAction } from "next-safe-action/hook";
import { useEffect, useState } from "react";
import { AssignedUser } from "./assigned-user";

export function AssignUser({ id, selectedId, isLoading }) {
const action = useAction(updateTransactionAction);
const [value, setValue] = useState();
const supabase = createClient();
const [users, setUsers] = useState([]);

const handleOnValueChange = (value: string) => {
startTransition(() => {
updateTransactionAction(id, {
assigned_id: value,
});
});
};

useEffect(() => {
setValue(selectedId);
}, [selectedId]);
Expand All @@ -53,7 +49,15 @@ export function AssignUser({ id, selectedId, isLoading }) {
<Skeleton className="h-[14px] w-[60%] rounded-sm absolute left-3 top-[39px]" />
</div>
) : (
<Select value={value} onValueChange={handleOnValueChange}>
<Select
value={value}
onValueChange={(assigned_id) => {
action.execute({
id,
assigned_id,
});
}}
>
<SelectTrigger id="assign" className="line-clamp-1 truncate">
<SelectValue placeholder="Select" />
</SelectTrigger>
Expand Down
53 changes: 26 additions & 27 deletions apps/dashboard/src/components/modals/feedback-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { sendFeeback } from "@/actions";
import { sendFeebackAction } from "@/actions/send-feedback-action";
import { Button } from "@midday/ui/button";
import {
Dialog,
Expand All @@ -11,24 +11,18 @@ import {
} from "@midday/ui/dialog";
import { Textarea } from "@midday/ui/textarea";
import { Loader2 } from "lucide-react";
import { useAction } from "next-safe-action/hook";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { useFormStatus } from "react-dom";

function SubmitButton() {
const { pending } = useFormStatus();

return (
<Button type="submit">
{pending ? <Loader2 className="h-4 w-4 animate-spin" /> : "Send"}
</Button>
);
}

export function FeedbackModal() {
const action = useAction(sendFeebackAction, {
onSuccess: () => {
setValue("");
},
});
const searchParams = useSearchParams();
const router = useRouter();
const [isSubmitted, setSubmitted] = useState(false);
const [value, setValue] = useState("");
const isOpen = searchParams.has("feedback");

Expand All @@ -38,14 +32,16 @@ export function FeedbackModal() {
<div className="p-4">
<DialogHeader>
<DialogTitle>Send feedback</DialogTitle>
<DialogDescription>
How can we improve Midday? If you have a feature request, can you
also share why it's important to you?
</DialogDescription>
{action.status !== "hasSucceeded" && (
<DialogDescription>
How can we improve Midday? If you have a feature request, can
you also share why it's important to you?
</DialogDescription>
)}
</DialogHeader>

<div className="mt-6">
{isSubmitted ? (
{action.status === "hasSucceeded" ? (
<div className="min-h-[100px] flex items-center justify-center flex-col space-y-1">
<p className="font-medium text-sm">
Thank you for your feedback!
Expand All @@ -55,14 +51,7 @@ export function FeedbackModal() {
</p>
</div>
) : (
<form
className="space-y-4"
action={async (formData) => {
await sendFeeback(formData);
setSubmitted(true);
setValue("");
}}
>
<form className="space-y-4">
<Textarea
name="feedback"
value={value}
Expand All @@ -74,7 +63,17 @@ export function FeedbackModal() {
/>

<div className="mt-1 flex items-center justify-end">
<SubmitButton />
<Button
type="button"
onClick={() => action.execute({ feedback: value })}
disabled={action.status === "executing"}
>
{action.status === "executing" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Send"
)}
</Button>
</div>
</form>
)}
Expand Down
21 changes: 10 additions & 11 deletions apps/dashboard/src/components/note.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
"use client";

import { updateTransactionAction } from "@/actions";
import { updateTransactionAction } from "@/actions/update-transaction-action";
import { Textarea } from "@midday/ui/textarea";
import { startTransition, useState } from "react";
import { useAction } from "next-safe-action/hook";
import { useState } from "react";

export function Note({ id, defaultValue }) {
const [value, setValue] = useState(defaultValue);

const handleOnBlur = () => {
startTransition(() => {
updateTransactionAction(id, {
note: value,
});
});
};
const action = useAction(updateTransactionAction);

return (
<Textarea
Expand All @@ -23,7 +17,12 @@ export function Note({ id, defaultValue }) {
autoFocus
placeholder="Note"
className="min-h-[100px] resize-none"
onBlur={handleOnBlur}
onBlur={() =>
action.execute({
id,
note: value,
})
}
onChange={(evt) => setValue(evt.target.value)}
/>
);
Expand Down
Loading

0 comments on commit 42bef26

Please sign in to comment.