SPB Git
8.6 KB · 77 lines typescript
1"use client";2import { createContext, useContext, useEffect, useState } from "react";3import { useQuery, useMutation, useQueryClient, keepPreviousData } from "@tanstack/react-query";4import { api } from "@/lib/client/api";5import { toast } from "@/components/ui/toast";6import type { Database, Property, View, ViewConfig, RowOut, QueryResult, FilterGroup, Sort, SelectOption } from "@/lib/databases/types";7import { isPending } from "@/lib/databases/types";8import { sortedProps, titleProp } from "@/lib/databases/values";910export interface DbCtx {11  db: Database; props: Property[]; visibleProps: Property[]; view: View; rows: RowOut[]; total: number; loading: boolean; isMobile: boolean; search: string;12  setCell: (rowId: string, values: Record<string, unknown>) => void; addRow: (values?: Record<string, unknown>, title?: string) => Promise<RowOut>; removeRows: (ids: string[]) => Promise<void>;13  open: (rowId: string | null) => void; patchView: (config: Partial<ViewConfig>) => void; refresh: () => void; regenerate: (rowId: string, propertyId: string) => Promise<void>; addOption: (propId: string, name: string) => Promise<SelectOption>;14}15export const DbContext = createContext<DbCtx | null>(null);16export const useDb = () => { const c = useContext(DbContext); if (!c) throw new Error("DbContext missing"); return c; };1718export const useDatabase = (id: string) => useQuery<Database>({ queryKey: ["db", id], queryFn: () => api(`/api/databases/${id}`) });19export function useRows(dbId: string | undefined, viewId: string | undefined, search: string, over: { filter?: FilterGroup | null; sorts?: Sort[] } = {}) {20  const q = useQuery<QueryResult>({ queryKey: ["db-rows", dbId, viewId, search, over], enabled: !!dbId && !!viewId, placeholderData: keepPreviousData, queryFn: () => api(`/api/databases/${dbId}/query`, { method: "POST", json: { viewId, search: search || undefined, limit: 500, ...over } }),21    refetchInterval: (query) => (query.state.data?.rows.some((r) => Object.values(r.values).some(isPending)) ? 2500 : false) });22  return q;23}24const patchRows = (qc: ReturnType<typeof useQueryClient>, dbId: string, fn: (rows: RowOut[]) => RowOut[]) => qc.setQueriesData<QueryResult>({ queryKey: ["db-rows", dbId] }, (old) => (old ? { ...old, rows: fn(old.rows), total: old.total } : old));25export function useRowMutations(db: Database | undefined) {26  const qc = useQueryClient(); const dbId = db?.id ?? ""; const tp = db ? titleProp(db.properties) : undefined;27  const update = useMutation({28    mutationFn: ({ rowId, values }: { rowId: string; values: Record<string, unknown> }) => api<RowOut>(`/api/databases/${dbId}/rows`, { method: "PATCH", json: { id: rowId, values } }),29    onMutate: async ({ rowId, values }) => { await qc.cancelQueries({ queryKey: ["db-rows", dbId] }); const prev = qc.getQueriesData<QueryResult>({ queryKey: ["db-rows", dbId] }); patchRows(qc, dbId, (rows) => rows.map((r) => (r.id === rowId ? { ...r, values: { ...r.values, ...values }, title: tp && tp.id in values ? String(values[tp.id] ?? "") : r.title } : r))); return { prev }; },30    onError: (e: any, _v, ctx) => { ctx?.prev.forEach(([k, d]) => qc.setQueryData(k, d)); toast("Save failed", { description: e?.message, tone: "danger" }); },31    onSuccess: (row) => patchRows(qc, dbId, (rows) => rows.map((r) => (r.id === row.id ? row : r))),32  });33  const create = useMutation({34    mutationFn: (input: { values?: Record<string, unknown>; title?: string }) => api<RowOut>(`/api/databases/${dbId}/rows`, { method: "POST", json: input }),35    onSuccess: (row) => { patchRows(qc, dbId, (rows) => (rows.some((r) => r.id === row.id) ? rows : [...rows, row])); qc.invalidateQueries({ queryKey: ["db-rows", dbId] }); },36    onError: (e: any) => toast("Could not create row", { description: e?.message, tone: "danger" }),37  });38  const remove = useMutation({39    mutationFn: (ids: string[]) => api(`/api/databases/${dbId}/rows?ids=${ids.join(",")}`, { method: "DELETE" }),40    onMutate: async (ids) => { await qc.cancelQueries({ queryKey: ["db-rows", dbId] }); const prev = qc.getQueriesData<QueryResult>({ queryKey: ["db-rows", dbId] }); patchRows(qc, dbId, (rows) => rows.filter((r) => !ids.includes(r.id))); return { prev }; },41    onError: (e: any, _v, ctx) => { ctx?.prev.forEach(([k, d]) => qc.setQueryData(k, d)); toast("Delete failed", { description: e?.message, tone: "danger" }); },42    onSuccess: () => { qc.invalidateQueries({ queryKey: ["tree"] }); },43  });44  return { update, create, remove };45}46export function useViewMutations(db: Database | undefined, pageId: string) {47  const qc = useQueryClient(); const dbId = db?.id ?? ""; const inv = () => qc.invalidateQueries({ queryKey: ["db", pageId] });48  const patchView = useMutation({49    mutationFn: ({ id, ...patch }: { id: string; name?: string; type?: string; config?: Partial<ViewConfig>; isDefault?: boolean }) => api<View>(`/api/databases/${dbId}/views`, { method: "PATCH", json: { id, ...patch } }),50    onMutate: async (v) => { await qc.cancelQueries({ queryKey: ["db", pageId] }); qc.setQueryData<Database>(["db", pageId], (old) => old ? { ...old, views: old.views.map((x) => (x.id === v.id ? { ...x, ...(v.name ? { name: v.name } : {}), ...(v.type ? { type: v.type as any } : {}), config: { ...x.config, ...(v.config ?? {}) } } : x)) } : old); },51    onSuccess: (view) => { qc.setQueryData<Database>(["db", pageId], (old) => old ? { ...old, views: old.views.map((x) => (x.id === view.id ? view : x)) } : old); qc.invalidateQueries({ queryKey: ["db-rows", dbId] }); },52    onError: (e: any) => { toast("View update failed", { description: e?.message, tone: "danger" }); inv(); },53  });54  const createView = useMutation({ mutationFn: (input: { name: string; type: string; config?: ViewConfig }) => api<View>(`/api/databases/${dbId}/views`, { method: "POST", json: input }), onSuccess: inv });55  const deleteView = useMutation({ mutationFn: (id: string) => api(`/api/databases/${dbId}/views?id=${id}`, { method: "DELETE" }), onSuccess: inv, onError: (e: any) => toast(e?.message, { tone: "danger" }) });56  return { patchView, createView, deleteView, invalidate: inv };57}58export function usePropertyMutations(db: Database | undefined, pageId: string) {59  const qc = useQueryClient(); const dbId = db?.id ?? ""; const inv = () => { qc.invalidateQueries({ queryKey: ["db", pageId] }); qc.invalidateQueries({ queryKey: ["db-rows", dbId] }); };60  const create = useMutation({ mutationFn: (input: { name: string; type: string; config?: Record<string, unknown>; width?: number | null }) => api<Property>(`/api/databases/${dbId}/properties`, { method: "POST", json: input }), onSuccess: inv, onError: (e: any) => toast("Could not add property", { description: e?.message, tone: "danger" }) });61  const update = useMutation({62    mutationFn: ({ id, ...patch }: { id: string; name?: string; type?: string; config?: Record<string, unknown>; width?: number | null }) => api<Property>(`/api/databases/${dbId}/properties`, { method: "PATCH", json: { id, ...patch } }),63    onMutate: async (v) => { await qc.cancelQueries({ queryKey: ["db", pageId] }); qc.setQueryData<Database>(["db", pageId], (old) => old ? { ...old, properties: old.properties.map((p) => (p.id === v.id ? { ...p, ...(v.name !== undefined ? { name: v.name } : {}), ...(v.width !== undefined ? { width: v.width } : {}), ...(v.config ? { config: v.config as any } : {}) } : p)) } : old); },64    onSuccess: inv, onError: (e: any) => { toast("Property update failed", { description: e?.message, tone: "danger" }); inv(); },65  });66  const remove = useMutation({ mutationFn: (id: string) => api(`/api/databases/${dbId}/properties?id=${id}`, { method: "DELETE" }), onSuccess: inv, onError: (e: any) => toast(e?.message, { tone: "danger" }) });67  const reorder = useMutation({ mutationFn: (order: string[]) => api(`/api/databases/${dbId}/properties`, { method: "PATCH", json: { order } }), onMutate: (order) => { qc.setQueryData<Database>(["db", pageId], (old) => old ? { ...old, properties: sortedProps(old.properties).map((p) => ({ ...p, position: order.indexOf(p.id) + 1 })) } : old); }, onSuccess: inv });68  return { create, update, remove, reorder };69}70export function useMedia(query: string) {71  const [m, setM] = useState(false);72  useEffect(() => { const mq = matchMedia(query); const h = () => setM(mq.matches); h(); mq.addEventListener("change", h); return () => mq.removeEventListener("change", h); }, [query]);73  return m;74}75export const useAllDatabases = () => useQuery<{ id: string; pageId: string; name: string; icon: string | null; rowCount: number; isTemplate: boolean }[]>({ queryKey: ["databases"], queryFn: () => api("/api/databases") });76export const useRemoteDatabase = (id: string | undefined | null) => useQuery<Database>({ queryKey: ["db", id], enabled: !!id, queryFn: () => api(`/api/databases/${id}`) });77
Files · main
Menu

Theme