Skip to content

Commit

Permalink
Merge pull request midday-ai#229 from midday-ai/feature/bulk-actions
Browse files Browse the repository at this point in the history
Bulk actions
  • Loading branch information
pontusab authored Aug 18, 2024
2 parents 592b0c4 + deb4544 commit 2343269
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 186 deletions.
2 changes: 1 addition & 1 deletion apps/dashboard/src/app/api/gocardless/reconnect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function GET(req: NextRequest) {
const supabase = createClient();
const requestUrl = new URL(req.url);
const id = requestUrl.searchParams.get("id");
const referenceId = requestUrl.searchParams.get("reference_id");
const referenceId = requestUrl.searchParams.get("reference_id") ?? undefined;
const isDesktop = requestUrl.searchParams.get("desktop");

if (id) {
Expand Down
5 changes: 3 additions & 2 deletions apps/dashboard/src/app/api/proxy/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getSession } from "@midday/supabase/cached-queries";
import type { NextRequest } from "next/server";

export async function GET(req) {
export async function GET(req: NextRequest) {
const requestUrl = new URL(req.url);
const filePath = requestUrl.searchParams.get("filePath");

Expand All @@ -12,7 +13,7 @@ export async function GET(req) {
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/${filePath}`,
{
headers: {
authorization: `Bearer ${session.access_token}`,
authorization: `Bearer ${session?.access_token}`,
},
},
);
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/assign-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type User = {
};

type Props = {
selectedId: string;
selectedId?: string;
isLoading: boolean;
onSelect: (user?: User) => void;
};
Expand All @@ -42,7 +42,7 @@ export function AssignUser({ selectedId, isLoading, onSelect }: Props) {
if (userData?.team_id) {
const { data: membersData } = await getTeamMembersQuery(
supabase,
userData.team_id
userData.team_id,
);

setUsers(membersData?.map(({ user }) => user));
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/assigned-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Props = {

export function AssignedUser({ avatarUrl, fullName }: Props) {
return (
<div className="flex space-x-2">
<div className="flex space-x-2 items-center">
{avatarUrl && (
<Avatar className="h-5 w-5">
<AvatarImage src={avatarUrl} alt={fullName ?? ""} />
Expand Down
317 changes: 151 additions & 166 deletions apps/dashboard/src/components/bulk-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,28 @@
import { bulkUpdateTransactionsAction } from "@/actions/bulk-update-transactions-action";
import { useTransactionsStore } from "@/store/transactions";
import { Button } from "@midday/ui/button";
import { cn } from "@midday/ui/cn";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@midday/ui/dropdown-menu";
import { Icons } from "@midday/ui/icons";
import { Label } from "@midday/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@midday/ui/popover";
import { RadioGroup, RadioGroupItem } from "@midday/ui/radio-group";
import { useToast } from "@midday/ui/use-toast";
import * as Tabs from "@radix-ui/react-tabs";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useAction } from "next-safe-action/hooks";
import { useState } from "react";
import { AssignUser } from "./assign-user";
import { SelectCategory } from "./select-category";
import { SelectUser } from "./select-user";

const sections = [
{
id: "categories",
label: "Categories",
icon: Icons.Category,
},
{
id: "transactions",
label: "Transactions",
icon: Icons.Visibility,
},
{
id: "assign",
label: "Assign",
icon: Icons.Face,
},
{
id: "status",
label: "Status",
icon: Icons.AlertCircle,
},
];
type Props = {
ids: string[];
};

export function BulkActions({ ids }) {
const [activeId, setActiveId] = useState(sections?.at(0)?.id as string);
const [isOpen, setOpen] = useState(false);
export function BulkActions({ ids }: Props) {
const { toast } = useToast();

const { setTransactionIds } = useTransactionsStore();
Expand Down Expand Up @@ -70,145 +53,147 @@ export function BulkActions({ ids }) {
});

return (
<Popover open={isOpen} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="space-x-2">
<span>Actions</span>
<ChevronDown size={16} />
<Icons.ChevronDown size={16} />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[500px] mt-2.5 p-0 overflow-hidden"
align="end"
>
<Tabs.Root
defaultValue="categories"
className="flex flex-row divide-x-[1px]"
onValueChange={setActiveId}
value={activeId}
>
<Tabs.TabsList className="w-[220px] h-[210px] p-4 flex flex-col items-start">
{sections?.map(({ id, label, icon: Icon }) => {
const isActive = activeId === id;

return (
<Tabs.TabsTrigger value={id} asChild key={id}>
<Button
className={cn(
"w-[190px] items-center justify-start relative mb-1.5 group",
isActive && "bg-secondary",
)}
variant="ghost"
>
{Icon && <Icon size={16} />}
<p
className={cn(
"p-sm font-normal ml-2 text-primary",
isActive && "bg-secondary",
)}
>
{label}
</p>
<ChevronRight
size={16}
className={cn(
"absolute right-2 invisible group-hover:visible",
isActive && "visible",
)}
/>
</Button>
</Tabs.TabsTrigger>
);
})}
</Tabs.TabsList>

{sections?.map((section) => {
return (
<Tabs.TabsContent
value={section.id}
className="p-4 space-y-4 w-full"
key={section.id}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[180px]" sideOffset={8}>
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icons.Category className="mr-2 h-4 w-4" />
<span>Categories</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent
sideOffset={14}
className="p-0 w-[250px] h-[270px]"
>
{section.id === "categories" && (
<SelectCategory
placeholder="Category"
onChange={(category) => {
bulkUpdateTransactions.execute({
type: "category",
data: ids.map((transaction) => ({
id: transaction,
category_slug: category.slug,
})),
});
}}
/>
)}
<SelectCategory
onChange={(selected) => {
bulkUpdateTransactions.execute({
type: "category",
data: ids.map((transaction) => ({
id: transaction,
category_slug: selected.slug,
})),
});
}}
headless
/>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>

{section.id === "transactions" && (
<RadioGroup
defaultValue="include"
onValueChange={(status) => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status,
})),
});
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="posted" id="posted" />
<Label htmlFor="posted">Include</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="excluded" id="excluded" />
<Label htmlFor="excluded">Exclude</Label>
</div>
</RadioGroup>
)}
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icons.Visibility className="mr-2 h-4 w-4" />
<span>Visibility</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={14}>
<DropdownMenuCheckboxItem
onCheckedChange={() => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status: "posted",
})),
});
}}
>
Include
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
onCheckedChange={() => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status: "excluded",
})),
});
}}
>
Exclude
</DropdownMenuCheckboxItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>

{section.id === "assign" && (
<AssignUser
onSelect={(userId) => {
bulkUpdateTransactions.execute({
type: "assigned",
data: ids.map((transaction) => ({
id: transaction,
assigned_id: userId,
})),
});
}}
/>
)}
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icons.Face className="mr-2 h-4 w-4" />
<span>Assign</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent
sideOffset={14}
className="w-[230px] h-[170px] p-4"
>
<SelectUser
onSelect={(selected) => {
bulkUpdateTransactions.execute({
type: "assigned",
data: ids.map((transaction) => ({
id: transaction,
assigned_id: selected?.id,
})),
});
}}
/>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>

{section.id === "status" && (
<RadioGroup
onValueChange={(status) => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status,
})),
});
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="completed" id="completed" />
<Label htmlFor="completed">Completed</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="posted" id="posted" />
<Label htmlFor="posted">Uncompleted</Label>
</div>
</RadioGroup>
)}
</Tabs.TabsContent>
);
})}
</Tabs.Root>
</PopoverContent>
</Popover>
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Icons.AlertCircle className="mr-2 h-4 w-4" />
<span>Status</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={14}>
<DropdownMenuCheckboxItem
onCheckedChange={() => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status: "completed",
})),
});
}}
>
Completed
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
onCheckedChange={() => {
bulkUpdateTransactions.execute({
type: "status",
data: ids.map((transaction) => ({
id: transaction,
status: "posted",
})),
});
}}
>
Uncompleted
</DropdownMenuCheckboxItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
Loading

0 comments on commit 2343269

Please sign in to comment.