Skip to content

Commit

Permalink
example of a better pattern for setting the active protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
jthrilly committed Nov 20, 2023
1 parent ea4174f commit 8e3b1ba
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';
import { api } from '~/trpc/client';
import { BadgeCheck } from 'lucide-react';

const ActiveButton = ({
active,
protocolId,
}: {
active: boolean;
protocolId: string;
}) => {
const utils = api.useUtils();

const { mutateAsync: setActiveProtocol } =
api.protocol.active.set.useMutation({
// Optimistic update
onMutate: async (newActiveProtocolId: string) => {
await utils.protocol.get.all.cancel();
await utils.protocol.active.get.cancel();

const protocolGetAll = utils.protocol.get.all.getData();
const protocolActiveGet = utils.protocol.active.get.getData();

utils.protocol.active.get.setData(undefined, protocolId);
utils.protocol.get.all.setData(
undefined,
(protocolGetAll) =>
protocolGetAll?.map((protocol) => {
if (protocol.id === newActiveProtocolId) {
return {
...protocol,
active: true,
};
}

return {
...protocol,
active: false,
};
}),
);

return { protocolGetAll, protocolActiveGet };
},
onSettled: () => {
void utils.protocol.get.all.invalidate();
void utils.protocol.active.get.invalidate();
},
onError: (_error, _newActiveProtocolId, context) => {
utils.protocol.get.all.setData(undefined, context?.protocolGetAll);
utils.protocol.active.get.setData(
undefined,
context?.protocolActiveGet,
);
},
});

if (active) {
return <BadgeCheck className="fill-white text-purple-500" />;
}

return (
<button
title="Make active..."
onClick={() => setActiveProtocol(protocolId)}
>
<BadgeCheck className="cursor-pointer fill-white text-primary/20 hover:scale-150 hover:fill-purple-500 hover:text-white" />
</button>
);
};

export default ActiveButton;
87 changes: 37 additions & 50 deletions app/(dashboard)/dashboard/_components/ProtocolsTable/Columns.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
'use client';

import { type ColumnDef, flexRender } from '@tanstack/react-table';

import { Checkbox } from '~/components/ui/checkbox';

import ActiveButton from './ActiveButton';
import { DataTableColumnHeader } from '~/components/DataTable/ColumnHeader';

import ActiveProtocolSwitch from '~/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch';

import type { ProtocolWithInterviews } from '~/shared/types';
import { dateOptions } from '~/components/DataTable/helpers';

export const ProtocolColumns = (): ColumnDef<ProtocolWithInterviews>[] => [
export const ProtocolColumns: ColumnDef<ProtocolWithInterviews>[] = [
{
id: 'select',
header: ({ table }) => (
Expand All @@ -30,45 +28,48 @@ export const ProtocolColumns = (): ColumnDef<ProtocolWithInterviews>[] => [
enableSorting: false,
enableHiding: false,
},
{
id: 'active',
enableSorting: true,
accessorFn: (row) => row.active,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Active" />
),
cell: ({ row }) => (
<ActiveButton active={row.original.active} protocolId={row.original.id} />
),
},
{
accessorKey: 'name',
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Name" />;
},
cell: ({ row }) => {
return (
<div className={row.original.active ? '' : 'text-muted-foreground'}>
{flexRender(row.original.name, row)}
</div>
);
return flexRender(row.original.name, row);
},
},
{
accessorKey: 'description',
header: 'Description',
cell: ({ row }) => {
return (
<div
className={
row.original.active
? 'min-w-[200px]'
: 'min-w-[200px] text-muted-foreground'
}
key={row.original.description}
>
{flexRender(row.original.description, row)}
</div>
);
return flexRender(row.original.description, row);
},
},
{
accessorKey: 'importedAt',
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Imported" />;
},
cell: ({ row }) => (
<div className={row.original.active ? '' : 'text-muted-foreground'}>
{new Date(row.original.importedAt).toLocaleString()}
cell: ({
row,
table: {
options: { meta },
},
}) => (
<div className="text-xs">
{new Intl.DateTimeFormat(meta.navigatorLanguages, dateOptions).format(
new Date(row.original.importedAt),
)}
</div>
),
},
Expand All @@ -77,31 +78,17 @@ export const ProtocolColumns = (): ColumnDef<ProtocolWithInterviews>[] => [
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Modified" />;
},
cell: ({ row }) => (
<div className={row.original.active ? '' : 'text-muted-foreground'}>
{new Date(row.original.lastModified).toLocaleString()}
</div>
),
},
{
accessorKey: 'schemaVersion',
header: 'Schema Version',
cell: ({ row }) => (
<div className={row.original.active ? '' : 'text-muted-foreground'}>
{row.original.schemaVersion}
cell: ({
row,
table: {
options: { meta },
},
}) => (
<div className="text-xs">
{new Intl.DateTimeFormat(meta.navigatorLanguages, dateOptions).format(
new Date(row.original.lastModified),
)}
</div>
),
},
{
accessorKey: 'active',
header: 'Active',
cell: ({ row }) => {
return (
<ActiveProtocolSwitch
initialData={row.original.active}
hash={row.original.hash}
/>
);
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const ProtocolsTable = ({
<>
{isLoading && <div>Loading...</div>}
<DataTable
columns={ProtocolColumns()}
columns={ProtocolColumns}
data={protocols}
filterColumnAccessorKey="name"
handleDeleteSelected={handleDelete}
Expand Down
30 changes: 28 additions & 2 deletions components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import {
flexRender,
getCoreRowModel,
Expand All @@ -9,8 +10,9 @@ import {
type ColumnFiltersState,
type SortingState,
type Row,
type Table as TTable,
} from '@tanstack/react-table';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Button } from '~/components/ui/Button';
import { Input } from '~/components/ui/Input';
import {
Expand All @@ -24,6 +26,15 @@ import {
import { makeDefaultColumns } from '~/components/DataTable/DefaultColumns';
import { Loader } from 'lucide-react';

type CustomTable<TData> = TTable<TData> & {
options?: {
meta?: {
getRowClasses?: (row: Row<TData>) => string | undefined;
navigatorLanguages?: string[];
};
};
};

interface DataTableProps<TData, TValue> {
columns?: ColumnDef<TData, TValue>[];
data: TData[];
Expand All @@ -44,6 +55,15 @@ export function DataTable<TData, TValue>({
const [sorting, setSorting] = useState<SortingState>([]);
const [isDeleting, setIsDeleting] = useState(false);
const [rowSelection, setRowSelection] = useState({});
const [navigatorLanguages, setNavigatorLanguages] = useState<
string[] | undefined
>();

useEffect(() => {
if (window.navigator.languages) {
setNavigatorLanguages(window.navigator.languages as string[]);
}
}, []);

const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);

Expand Down Expand Up @@ -92,12 +112,17 @@ export function DataTable<TData, TValue>({
onRowSelectionChange: setRowSelection,
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
meta: {
getRowClasses: (row) =>
row.original.active && 'bg-purple-500/30 hover:bg-purple-500/40',
navigatorLanguages,
},
state: {
sorting,
rowSelection,
columnFilters,
},
});
}) as CustomTable<TData>;

const hasSelectedRows = table.getSelectedRowModel().rows.length > 0;

Expand Down Expand Up @@ -147,6 +172,7 @@ export function DataTable<TData, TValue>({
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className={table.options.meta?.getRowClasses?.(row)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
Expand Down
8 changes: 2 additions & 6 deletions components/DataTable/DefaultColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { type ColumnDef } from '@tanstack/react-table';

export const makeDefaultColumns = <TData, TValue>(
data: TData[],
): ColumnDef<TData, TValue>[] => {
export const makeDefaultColumns = <TData,>(data: TData[]) => {
const firstRow = data[0];

if (!firstRow || typeof firstRow !== 'object') {
Expand All @@ -11,7 +7,7 @@ export const makeDefaultColumns = <TData, TValue>(

const columnKeys = Object.keys(firstRow);

const columns: ColumnDef<TData, TValue>[] = columnKeys.map((key) => {
const columns = columnKeys.map((key) => {
return {
accessorKey: key,
header: key,
Expand Down
8 changes: 8 additions & 0 deletions components/DataTable/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Display options for dates: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options
export const dateOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
};
36 changes: 36 additions & 0 deletions components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "~/utils/shadcn"

const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}

export { Badge, badgeVariants }
1 change: 0 additions & 1 deletion declarations.d.ts
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ datasource db {

model Protocol {
id String @id @default(cuid())
active Boolean @default(false)
hash String @unique
name String
schemaVersion Int
Expand All @@ -24,7 +25,6 @@ model Protocol {
codebook Json
assets Asset[]
interviews Interview[]
active Boolean @default(false)
}

model Asset {
Expand Down
Loading

0 comments on commit 8e3b1ba

Please sign in to comment.