Skip to content

Commit

Permalink
feat: add view CRUD actions
Browse files Browse the repository at this point in the history
  • Loading branch information
nainglinnkhant committed Jul 5, 2024
1 parent e3da22d commit 181e8a5
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 32 deletions.
12 changes: 9 additions & 3 deletions src/app/_components/tasks-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import { useDataTable } from "@/hooks/use-data-table"
import { DataTableAdvancedToolbar } from "@/components/data-table/advanced/data-table-advanced-toolbar"
import { DataTable } from "@/components/data-table/data-table"

import type { getTasks } from "../_lib/queries"
import type { getTasks, getViews } from "../_lib/queries"
import { getPriorityIcon, getStatusIcon } from "../_lib/utils"
import { getColumns } from "./tasks-table-columns"
import { TasksTableFloatingBar } from "./tasks-table-floating-bar"
import { TasksTableToolbarActions } from "./tasks-table-toolbar-actions"

interface TasksTableProps {
tasksPromise: ReturnType<typeof getTasks>
viewsPromise: ReturnType<typeof getViews>
}

export function TasksTable({ tasksPromise }: TasksTableProps) {
export function TasksTable({ tasksPromise, viewsPromise }: TasksTableProps) {
const { data, pageCount } = React.use(tasksPromise)
const views = React.use(viewsPromise)

// Memoize the columns so they don't re-render on every render
const columns = React.useMemo(() => getColumns(), [])
Expand Down Expand Up @@ -78,7 +80,11 @@ export function TasksTable({ tasksPromise }: TasksTableProps) {
table={table}
floatingBar={<TasksTableFloatingBar table={table} />}
>
<DataTableAdvancedToolbar table={table} filterFields={filterFields}>
<DataTableAdvancedToolbar
table={table}
filterFields={filterFields}
views={views}
>
<TasksTableToolbarActions table={table} />
</DataTableAdvancedToolbar>
</DataTable>
Expand Down
205 changes: 203 additions & 2 deletions src/app/_lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@

import { unstable_noStore as noStore, revalidatePath } from "next/cache"
import { db } from "@/db"
import { tasks, type Task } from "@/db/schema"
import { tasks, views, type Task } from "@/db/schema"
import { takeFirstOrThrow } from "@/db/utils"
import { asc, eq, inArray, not } from "drizzle-orm"
import { customAlphabet } from "nanoid"

import { getErrorMessage } from "@/lib/handle-error"

import { generateRandomTask } from "./utils"
import type { CreateTaskSchema, UpdateTaskSchema } from "./validations"
import {
createViewSchema,
deleteViewSchema,
editViewSchema,
type CreateTaskSchema,
type CreateViewSchema,
type DeleteViewSchema,
type EditViewSchema,
type UpdateTaskSchema,
} from "./validations"

export type CreateFormState<T> = {
status?: "success" | "error"
message?: string
errors?: Partial<Record<keyof T, string>>
}

export async function createTask(input: CreateTaskSchema) {
noStore()
Expand Down Expand Up @@ -161,3 +176,189 @@ export async function deleteTasks(input: { ids: string[] }) {
}
}
}

type CreateViewFormState = CreateFormState<CreateViewSchema>

export async function createView(
_prevState: CreateViewFormState,
formData: FormData
): Promise<CreateViewFormState> {
noStore()

const name = formData.get("name")
const columns = formData.get("columns")
? JSON.parse(formData.get("columns") as string)
: undefined
const filters = formData.get("filters")
? JSON.parse(formData.get("filters") as string)
: undefined

const validatedFields = createViewSchema.safeParse({
name,
columns,
filters,
})

if (!validatedFields.success) {
const errorMap = validatedFields.error.flatten().fieldErrors
return {
status: "error",
message: errorMap.name?.[0] ?? "",
}
}

try {
await db.transaction(async (tx) => {
const newView = await tx
.insert(views)
.values({
name: validatedFields.data.name,
columns: validatedFields.data.columns,
filters: validatedFields.data.filters,
})
.returning({
id: views.id,
})
.then(takeFirstOrThrow)

const allViews = await db.select({ id: views.id }).from(views)
if (allViews.length === 10) {
await tx.delete(views).where(
eq(
views.id,
(
await tx
.select({
id: views.id,
})
.from(views)
.limit(1)
.where(not(eq(views.id, newView.id)))
.orderBy(asc(views.createdAt))
.then(takeFirstOrThrow)
).id
)
)
}
})

revalidatePath("/")

return {
status: "success",
message: "View created",
}
} catch (err) {
return {
status: "error",
message: getErrorMessage(err),
}
}
}

type EditViewFormState = CreateFormState<EditViewSchema>

export async function editView(
_prevState: EditViewFormState,
formData: FormData
): Promise<EditViewFormState> {
noStore()

const id = formData.get("id")
const name = formData.get("name")
const columns = formData.get("columns")
? JSON.parse(formData.get("columns") as string)
: undefined
const filters = formData.get("filters")
? JSON.parse(formData.get("filters") as string)
: undefined

const validatedFields = editViewSchema.safeParse({
id,
name,
columns,
filters,
})

if (!validatedFields.success) {
const errorMap = validatedFields.error.flatten().fieldErrors
return {
status: "error",
message: errorMap.name?.[0] ?? "",
}
}

try {
await db
.update(views)
.set({
name: validatedFields.data.name,
columns: validatedFields.data.columns,
filters: validatedFields.data.filters,
})
.where(eq(views.id, validatedFields.data.id))

revalidatePath("/")

return {
status: "success",
message: "View updated",
}
} catch (err) {
if (
typeof err === "object" &&
err &&
"code" in err &&
err.code === "23505"
) {
return {
status: "error",
message: `A view with the name "${validatedFields.data.name}" already exists`,
}
}

return {
status: "error",
message: getErrorMessage(err),
}
}
}

type DeleteViewFormState = CreateFormState<DeleteViewSchema>

export async function deleteView(
_prevState: DeleteViewFormState,
formData: FormData
): Promise<DeleteViewFormState> {
noStore()

const id = formData.get("id")

const validatedFields = deleteViewSchema.safeParse({
id,
})

if (!validatedFields.success) {
const errorMap = validatedFields.error.flatten().fieldErrors
return {
status: "error",
message: errorMap.id?.[0] ?? "",
}
}

try {
await db.delete(views).where(eq(views.id, validatedFields.data.id))

revalidatePath("/")

return {
status: "success",
message: "View deleted",
}
} catch (err) {
return {
status: "error",
message: getErrorMessage(err),
}
}
}
14 changes: 13 additions & 1 deletion src/app/_lib/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "server-only"

import { unstable_noStore as noStore } from "next/cache"
import { db } from "@/db"
import { tasks, type Task } from "@/db/schema"
import { tasks, views, type Task } from "@/db/schema"
import type { DrizzleWhere } from "@/types"
import { and, asc, count, desc, gte, lte, or, sql, type SQL } from "drizzle-orm"

Expand Down Expand Up @@ -130,3 +130,15 @@ export async function getTaskCountByPriority() {
return []
}
}

export async function getViews() {
noStore()
return await db
.select({
id: views.id,
name: views.name,
columns: views.columns,
filters: views.filters,
})
.from(views)
}
28 changes: 28 additions & 0 deletions src/app/_lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const searchParamsSchema = z.object({
from: z.string().optional(),
to: z.string().optional(),
operator: z.enum(["and", "or"]).optional(),
viewId: z.string().uuid().optional(),
})

export type SearchParams = z.infer<typeof searchParamsSchema>
Expand All @@ -36,3 +37,30 @@ export const updateTaskSchema = z.object({
})

export type UpdateTaskSchema = z.infer<typeof updateTaskSchema>

export const createViewSchema = z.object({
name: z.string().min(1),
columns: z.string().array().optional(),
filters: z
.object({
field: z.enum(["title", "status", "priority"]),
value: z.string(),
isMulti: z.boolean().default(false),
})
.array()
.optional(),
})

export type CreateViewSchema = z.infer<typeof createViewSchema>

export const editViewSchema = createViewSchema.extend({
id: z.string().uuid(),
})

export type EditViewSchema = z.infer<typeof editViewSchema>

export const deleteViewSchema = z.object({
id: z.string().uuid(),
})

export type DeleteViewSchema = z.infer<typeof deleteViewSchema>
5 changes: 3 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DateRangePicker } from "@/components/date-range-picker"
import { Shell } from "@/components/shell"

import { TasksTable } from "./_components/tasks-table"
import { getTasks } from "./_lib/queries"
import { getTasks, getViews } from "./_lib/queries"
import { searchParamsSchema } from "./_lib/validations"

export interface IndexPageProps {
Expand All @@ -17,6 +17,7 @@ export default async function IndexPage({ searchParams }: IndexPageProps) {
const search = searchParamsSchema.parse(searchParams)

const tasksPromise = getTasks(search)
const viewsPromise = getViews()

return (
<Shell className="gap-2">
Expand All @@ -43,7 +44,7 @@ export default async function IndexPage({ searchParams }: IndexPageProps) {
* Passing promises and consuming them using React.use for triggering the suspense fallback.
* @see https://react.dev/reference/react/use
*/}
<TasksTable tasksPromise={tasksPromise} />
<TasksTable tasksPromise={tasksPromise} viewsPromise={viewsPromise} />
</React.Suspense>
</Shell>
)
Expand Down
Loading

0 comments on commit 181e8a5

Please sign in to comment.