feat: basic search functionality for feed table

This commit is contained in:
Corban-Lee Jones 2025-04-26 22:52:03 +01:00
parent 48cd87749e
commit f8724162ad
6 changed files with 57 additions and 52 deletions

View File

@ -17,6 +17,7 @@ app.engine("ejs", engine);
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.set("views", path.resolve(__dirname, "client/views")); app.set("views", path.resolve(__dirname, "client/views"));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use("/static", express.static(path.resolve(__dirname, "client/public"))); app.use("/static", express.static(path.resolve(__dirname, "client/public")));

View File

@ -46,8 +46,7 @@
} }
.cj-table-footer { .cj-table-footer {
@apply px-6 py-4 gap-3 flex justify-between items-center border-t @apply px-6 py-4 gap-3 flex justify-between items-center;
border-gray-200 dark:border-neutral-700;
} }
.cj-table-paging-btn { .cj-table-paging-btn {

View File

@ -23,7 +23,8 @@ const emptyTableHtml: string = `
No results found No results found
</h2> </h2>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400"> <p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
Create a feed and it will appear here. Refine your search or create a new feed.
Alternatively, use a template to deploy a ready-made feed.
</p> </p>
<div class="mt-5 flex flex-col sm:flex-row gap-2"> <div class="mt-5 flex flex-col sm:flex-row gap-2">
@ -108,8 +109,8 @@ const columnDefs: ConfigColumnDefs[] = [
{ {
target: 5, target: 5,
data: "message_style", data: "message_style",
orderable: true, orderable: false, // both should be true, but message_style doesnt exist yet
searchable: true, searchable: false,
render(data: string) { return ` render(data: string) { return `
<td class="size-px whitespace-nowrap align-top"> <td class="size-px whitespace-nowrap align-top">
<div class="px-6 py-4"> <div class="px-6 py-4">
@ -154,10 +155,13 @@ const columnDefs: ConfigColumnDefs[] = [
const ajaxSettings: AjaxSettings = { const ajaxSettings: AjaxSettings = {
url: `/guild/${1204426362794811453}/feeds/api/datatable`, url: `/guild/${1204426362794811453}/feeds/api/datatable`,
type: "POST",
contentType: "application/json",
dataSrc: "data", dataSrc: "data",
data: (data: unknown) => { data: (data: unknown) => {
if (data === undefined) return; if (data === undefined) return;
// TODO // TODO,
return JSON.stringify(data);
} }
}; };

View File

@ -9,8 +9,28 @@
<div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700"> <div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
<!-- Header --> <!-- Header -->
<div> <div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center">
placeholder header content
<div class="hidden sm:block sm:col-span-1">
<label for="search" class="sr-only">Search</label>
<div class="relative">
<input type="text" id="search" class="form-input px-3 ps-11 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="Search" data-hs-datatable-search="">
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4">
<svg class="shrink-0 size-4 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</div>
</div>
</div>
<div class="sm:col-span-2">
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
<span>
Create
<span class="hidden sm:inline">a feed</span>
</span>
</button>
</div>
</div> </div>
<!-- Table --> <!-- Table -->
@ -125,7 +145,7 @@
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg> <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
Prev Prev
</button> </button>
<div class="flex items-center space-x-1 " data-hs-datatable-paging-pages=""></div> <div class="flex items-center space-x-1" data-hs-datatable-paging-pages=""></div>
<button type="button" class="cj-table-paging-btn" data-hs-datatable-paging-next=""> <button type="button" class="cj-table-paging-btn" data-hs-datatable-paging-next="">
Next Next
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg> <svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>

View File

@ -1,5 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import prisma, { Prisma } from "@server/prisma"; import prisma, { Prisma } from "@server/prisma";
import { AjaxData, AjaxResponse } from "datatables.net-dt";
export const get = async (request: Request, response: Response) => { export const get = async (request: Request, response: Response) => {
if (!request.query.id) { if (!request.query.id) {
@ -76,61 +77,41 @@ export const del = async (request: Request, response: Response) => {
response.status(204).json(null); response.status(204).json(null);
}; };
interface DataTableResponse {
data: any;
recordsFiltered: number;
recordsTotal: number;
}
interface DatatableQuery { interface DatatableQuery extends AjaxData {
length: string;
start: string;
order: { column: string; dir: string }[];
columns: { [key: string]: { data: string; searchable: string }};
search: { value: string };
filters: { [key: string]: any }; filters: { [key: string]: any };
} }
export const datatable = async (request: Request, response: Response) => { export const datatable = async (request: Request, response: Response) => {
const query = request.query as unknown as DatatableQuery; const query = request.body as unknown as DatatableQuery;
const size: number = Number(query.length) || 10; const orderBy: Prisma.FeedOrderByWithRelationInput = query.order?.length
const start: number = Number(query.start); ? { [query.columns[query.order[0].column].data]: query.order[0].dir }
const order: string = (query.order && query.columns[query.order[0].column].data) || "id"; : { id: "asc" };
const direction: string = (query.order && query.order[0].dir) || "asc";
const search: string = query.search?.value || "";
let dbQuery: any = {}; const where: Prisma.FeedWhereInput = query.search?.value
? {
// TODO: filter request OR: Object.values(query.columns)
.filter(col => col.searchable)
if (search) { .map(col => ({
Object.values(query.columns) [col.data]: { contains: query.search.value }
.filter(column => column.searchable === "true") })) as Prisma.FeedWhereInput[]
.forEach((col: any) => { }
dbQuery["where"][col.data] = { : {};
contains: search,
mode: "insensitive"
}
}
);
}
const orderBy: any = {};
orderBy[order] = direction;
const data = await prisma.feed.findMany({ const data = await prisma.feed.findMany({
...dbQuery, skip: query.start,
skip: start, take: query.length,
take: size,
orderBy: orderBy, orderBy: orderBy,
include: { channels: true } where: where,
include: { channels: true },
}); });
response.json(<DataTableResponse>{ response.json(<AjaxResponse>{
data: data, data: data,
recordsFiltered: await prisma.feed.count({...dbQuery}), recordsFiltered: await prisma.feed.count({ where: where }),
recordsTotal: await prisma.feed.count() recordsTotal: await prisma.feed.count(),
draw: query.draw
}); });
}; };

View File

@ -24,7 +24,7 @@ router.get("/:guildId/content", contentController.get);
// API routes // API routes
router.get("/:guildId/feeds/api/datatable", feedApiController.datatable); router.post("/:guildId/feeds/api/datatable", feedApiController.datatable);
router.get("/:guildId/feeds/api", feedApiController.get); router.get("/:guildId/feeds/api", feedApiController.get);
router.post("/:guildId/feeds/api", feedApiController.post); router.post("/:guildId/feeds/api", feedApiController.post);
router.patch("/:guildId/feeds/api", feedApiController.patch); router.patch("/:guildId/feeds/api", feedApiController.patch);