From 73aed35ce04180e7adc9043bdf9ab2193a9e4d74 Mon Sep 17 00:00:00 2001 From: Corban-Lee Jones Date: Fri, 2 May 2025 13:43:02 +0100 Subject: [PATCH] feat: completed filter table and edit modal --- src/client/src/ts/guild/filters.ts | 433 ++++++++++++------ src/client/views/guild/filters.ejs | 163 ++++--- .../controllers/guild/filter.controller.ts | 12 +- 3 files changed, 408 insertions(+), 200 deletions(-) diff --git a/src/client/src/ts/guild/filters.ts b/src/client/src/ts/guild/filters.ts index a87dba1..ffe6468 100644 --- a/src/client/src/ts/guild/filters.ts +++ b/src/client/src/ts/guild/filters.ts @@ -1,15 +1,19 @@ import $ from "jquery"; -import HSSelect, { ISelectOptions } from "@preline/select"; +import "datatables.net-select-dt"; +import HSDropdown from "@preline/dropdown"; +import HSOverlay, { IOverlayOptions } from "@preline/overlay"; +import HSSelect, { ISelectOptions, ISingleOption } from "@preline/select"; import HSDataTable, { IDataTableOptions } from "@preline/datatable"; -import DataTable from "datatables.net"; -import { ConfigColumnDefs, AjaxSettings } from "datatables.net-dt"; +import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt"; import { autoUpdate, computePosition, offset } from "@floating-ui/dom"; -import { formatTimestamp } from "../../../src/ts/main"; +import { formatTimestamp } from "../main"; +import prisma from "../../../../../generated/prisma"; + +declare let guildId: string; +declare const matchingAlgorithms: { [key: string]: string }; // #region DataTable -// -// Fix dependency bugs with preline (window as any).DataTable = DataTable; (window as any).$hsDataTableCollection = []; @@ -39,155 +43,128 @@ const emptyTableHtml: string = ` `; const columnDefs: ConfigColumnDefs[] = [ - // Select checkbox column - { - target: 0, - orderable: false, - searchable: false, - render: (_data: unknown, _type: unknown, row: any) => { return ` - + // Select checkbox column + { + target: 0, + orderable: false, + searchable: false, + className: "size-px whitespace-nowrap", + render: (_data: unknown, _type: unknown, row: prisma.Filter) => { return `
- - `} - }, - { - target: 1, - data: "name", - orderable: true, - searchable: true, - render: (data: string) => { return ` - - + `} + }, + { + target: 1, + data: "name", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string, _type: string, row: prisma.Filter) => { return ` + ${data} - - `} - }, - { - target: 2, - data: "value", - orderable: true, - searchable: true, - render: (data: string) => { return ` - -
+ `} + }, + { + target: 2, + data: "value", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string) => { return ` +
${data}
- - `} - }, - { - target: 3, - data: "matching_algorithm", - orderable: true, - searchable: true, - render: (data: string) => { - const wrapper = $("
").addClass("px-6 py-4"); - const label = $("").addClass("cj-table-text"); - let description: string; + `} + }, + { + target: 3, + data: "matching_algorithm", + orderable: true, + searchable: true, + className: "size-px whitespace-nowrap", + render: (data: string, type: string) => { + if (type !== "display") return data; - switch (data) { - case "ANY": - description = "Any Word"; - break; - case "ALL": - description = "All Words"; - break; - case "EXACT": - description = "Exact Match"; - break; - case "REGEX": - description = "Regular Expression"; - break; - case "FUZZY": - description = "Fuzzy Match"; - break; + const wrapper = $("
").addClass("px-6 py-4"); + const badge = $("").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 border text-xs rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 text-gray-800 dark:text-neutral-200"); + + badge.text(matchingAlgorithms[data]); + + wrapper.append(badge); + return wrapper.get(0); } + }, + { + target: 4, + data: "is_insensitive", + orderable: true, + searchable: false, + className: "size-px whitespace-nowrap", + render: (data: boolean) => { + const wrapper = $("
").addClass("px-6 py-4"); + const badge = $("").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md"); - label.text(description); - wrapper.append(label); - return wrapper.get(0); - } - // render: (data: string) => { return ` - // - //
- // - // ${data} - // - //
- // - // `} - }, - { - target: 4, - data: "is_insensitive", - orderable: true, - searchable: true, - render: data => { - const wrapper = $("
").addClass("px-6 py-4"); - const badge = $("").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full"); - const label = $(""); - - if (data) { - badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500"); - badge.append(label.text("No")); - } else { - badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500"); - badge.append(label.text("Yes")); + if (data) { + badge.text("Case-Insensitive"); + badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500"); + } else { + badge.text("Case-Sensitive"); + badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500"); + } + + wrapper.append(badge); + return wrapper.get(0); } + }, + { + target: 5, + data: "is_whitelist", + orderable: true, + searchable: false, + className: "size-px whitespace-nowrap", + render: (data: boolean) => { + const wrapper = $("
").addClass("px-6 py-4"); + const badge = $("").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md"); - wrapper.append(badge); - return wrapper.get(0); - } - }, - { - target: 5, - data: "is_whitelist", - orderable: true, - searchable: true, - render: data => { - const wrapper = $("
").addClass("px-6 py-4"); - const badge = $("").addClass("py-1 px-2 inline-flex items-center text-xs font-medium rounded-full"); - const label = $(""); - - if (data) { - badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500"); - badge.append(label.text("Whitelist")); - } else { - badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500"); - badge.append(label.text("Blacklist")); + if (data) { + badge.text("Whitelist"); + badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500"); + } else { + badge.text("Blacklist"); + badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500"); + } + + wrapper.append(badge); + return wrapper.get(0); } - - wrapper.append(badge); - return wrapper.get(0); - } - }, - { - target: 6, - data: "created_at", - orderable: true, - searchable: false, - render: (data: string) => { return ` - + }, + { + target: 6, + data: "created_at", + orderable: true, + searchable: false, + className: "size-px whitespace-nowrap", + render: (data: string) => { return `
${formatTimestamp(data)}
- - `} - } + `} + }, + ]; const ajaxSettings: AjaxSettings = { - url: `/guild/${1204426362794811453}/filters/api/datatable`, + url: `/guild/${guildId}/filters/api/datatable`, type: "POST", contentType: "application/json", dataSrc: "data", @@ -202,6 +179,10 @@ const tableOptions: IDataTableOptions = { ajax: ajaxSettings, serverSide: true, processing: true, + select: { + style: "multi", + selector: "td:first-child input[type='checkbox']" + }, columnDefs: columnDefs, pagingOptions: { pageBtnClasses: "hidden" }, rowSelectingOptions: { selectAllSelector: "#selectAllBox" }, @@ -210,20 +191,60 @@ const tableOptions: IDataTableOptions = { emptyTable: emptyTableHtml, loadingRecords: "Placeholder loading message..." }, + drawCallback: () => HSDropdown.autoInit(), rowCallback: (row: HTMLTableRowElement) => { $(row).addClass("bg-white dark:bg-neutral-900"); } }; -const table: HSDataTable = new HSDataTable( - $("#table").get(0), +const table = new HSDataTable( + $("#table").get(0) as HTMLElement, tableOptions -); +) + +const onTableSelectChange = () => { + const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count(); + $("#deleteRowsBtn").prop("disabled", selectedRowsCount === 0); + $(".rows-selected-count-js").text(selectedRowsCount); + + const $elem = $(".rows-selected-count-js.zero-empty-js"); + selectedRowsCount === 0 ? $elem.hide() : $elem.show(); +}; + +(table as any).dataTable + .on("select", onTableSelectChange) + .on("deselect", onTableSelectChange) + .on("draw", onTableSelectChange); + +$("#selectAllBox").on("change", function() { + (this as HTMLInputElement).checked + ? (table as any).dataTable.rows().select() + : (table as any).dataTable.rows().deselect(); +}); + +$("#deleteRowsBtn").on("click", async () => { + const dt: Api = (table as any).dataTable; + const rowsData = dt.rows({ selected: true }).data().toArray(); + const rowIds = rowsData.map((row: prisma.Filter) => row.id); + + await $.ajax({ + url: `/guild/${guildId}/filters/api`, + method: "delete", + dataType: "json", + data: { ids: rowIds }, + success: () => { + dt.draw(); + dt.rows().deselect(); + }, + error: error => { + alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error); + } + }); +}); // #endregion // #region Page Size Select -// https://preline.co/plugins/html/advanced-select.html (window as any).$hsSelectCollection = []; (window as any)["FloatingUIDOM"] = { @@ -232,13 +253,19 @@ const table: HSDataTable = new HSDataTable( offset: offset }; +// Close on click. +window.addEventListener('click', (evt) => { + const evtTarget = evt.target; + HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement); +}); + const pageSelectOptions: ISelectOptions = { - toggleTag: "", + toggleTag: '', optionTemplate: ` -
+
- - +
`, toggleClasses: "cj-table-paging-select-toggle", @@ -251,8 +278,134 @@ const pageSelectOptions: ISelectOptions = { }; const pageSizeSelect: HSSelect = new HSSelect( - $("#selectPageSize-js").get(0), + $("#selectPageSize-js").get(0) as HTMLElement, pageSelectOptions ); +// #endregion + +// #region Edit Modal + +(window as any).$hsOverlayCollection = []; + +const editModalOptions: IOverlayOptions = {}; + +const editModal: HSOverlay = new HSOverlay( + $("#editModal").get(0) as HTMLElement, + editModalOptions +); + +$(document).on("click", ".open-edit-modal-js", async event => { + await openEditModal($(event.target).data("id")); +}); + +const clearEditModalData = () => { + $(editModal.el).removeData("id"); + + $("#formName").val(""); + $("#formValue").val(""); + $("#formInsensitive").prop("checked", false); + $("#formWhitelist").prop("checked", false); + + algorithmSelect.setValue(""); +}; + +const loadEditModalData = async (id: number) => { + const filter: prisma.Filter = await $.ajax({ + url: `/guild/${guildId}/filters/api?id=${id}`, + method: "get" + }); + + $(editModal.el).data("id", filter.id); + + $("#formName").val(filter.name); + $("#formValue").val(filter.value); + $("#formInsensitive").prop("checked", filter.is_insensitive); + $("#formWhitelist").prop("checked", filter.is_whitelist); + + // BUG: + // Breaks the appearance & functionality of the select + algorithmSelect.setValue(filter.matching_algorithm); +} + +const openEditModal = async (id: number | undefined) => { + $("#editForm").removeClass("submitted"); + editModal.open(); + + id === undefined + ? clearEditModalData() + : loadEditModalData(id); +}; + +const closeEditModal = () => { + editModal.close(); +}; + +const algorithmSelectOptions: ISelectOptions = { + toggleTag: '', + optionTemplate: ` +
+ + +
`, + toggleClasses: "cj-select-toggle select-input", + optionClasses: "cj-select-option", + dropdownClasses: "cj-select-dropdown", + wrapperClasses: "peer", + dropdownSpace: 10, + dropdownScope: "parent", + dropdownPlacement: "top", + dropdownVerticalFixedPlacement: null +}; + +const algorithmSelect = new HSSelect( + $("#formAlgorithm").get(0), + algorithmSelectOptions +); + +// Add options to algorithm select +Object.entries(matchingAlgorithms).forEach(([key, description]) => { + algorithmSelect.addOption({ + title: description, + val: key + } as ISingleOption) +}) + +$("#editForm").on("submit", async event => { + event.preventDefault(); + + const form = $(event.target).get(0) as HTMLFormElement; + $(form).addClass("submitted"); + + if (!form.checkValidity()) return; + + let method = "post"; + const data = $(event.target).serializeArray(); + const id: number | undefined = $(editModal.el).data("id"); + + if (id !== undefined) { + data.push({ + name: "id", + value: `${id}` + }) + method = "patch"; + } + + await $.ajax({ + url: `/guild/${guildId}/filters/api`, + dataType: "json", + method: method, + data: data, + success: () => { + (table as any).dataTable.draw(); + closeEditModal(); + }, + error: error => { + alert(JSON.stringify(error, null, 4)); + } + }); +}); + // #endregion \ No newline at end of file diff --git a/src/client/views/guild/filters.ejs b/src/client/views/guild/filters.ejs index 0cda4bf..ec255c0 100644 --- a/src/client/views/guild/filters.ejs +++ b/src/client/views/guild/filters.ejs @@ -22,6 +22,13 @@
+
@@ -156,17 +142,72 @@
-