feat: completed filter table and edit modal
All checks were successful
Build / build (push) Successful in 42s
All checks were successful
Build / build (push) Successful in 42s
This commit is contained in:
parent
e58d7343b1
commit
73aed35ce0
@ -1,15 +1,19 @@
|
|||||||
import $ from "jquery";
|
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 HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
||||||
import DataTable from "datatables.net";
|
import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
||||||
import { ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
|
||||||
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
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
|
// #region DataTable
|
||||||
//
|
|
||||||
|
|
||||||
// Fix dependency bugs with preline
|
|
||||||
(window as any).DataTable = DataTable;
|
(window as any).DataTable = DataTable;
|
||||||
(window as any).$hsDataTableCollection = [];
|
(window as any).$hsDataTableCollection = [];
|
||||||
|
|
||||||
@ -39,155 +43,128 @@ const emptyTableHtml: string = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const columnDefs: ConfigColumnDefs[] = [
|
const columnDefs: ConfigColumnDefs[] = [
|
||||||
// Select checkbox column
|
// Select checkbox column
|
||||||
{
|
{
|
||||||
target: 0,
|
target: 0,
|
||||||
orderable: false,
|
orderable: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
render: (_data: unknown, _type: unknown, row: any) => { return `
|
className: "size-px whitespace-nowrap",
|
||||||
<td class="size-px whitespace-nowrap">
|
render: (_data: unknown, _type: unknown, row: prisma.Filter) => { return `
|
||||||
<div class="ps-6 py-4">
|
<div class="ps-6 py-4">
|
||||||
<label class="rowSelect${row.id}-js" class="flex">
|
<label class="rowSelect${row.id}-js" class="flex">
|
||||||
<input type="checkbox" id="rowSelect${row.id}-js" class="cj-table-checkbox" data-hs-datatable-row-selecting-individual=""/>
|
<input type="checkbox" id="rowSelect${row.id}-js" class="cj-table-checkbox" data-hs-datatable-row-selecting-individual="">
|
||||||
<span class="sr-only">Checkbox</span>
|
<span class="sr-only">Select Row</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
`}
|
||||||
`}
|
},
|
||||||
},
|
{
|
||||||
{
|
target: 1,
|
||||||
target: 1,
|
data: "name",
|
||||||
data: "name",
|
orderable: true,
|
||||||
orderable: true,
|
searchable: true,
|
||||||
searchable: true,
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string) => { return `
|
render: (data: string, _type: string, row: prisma.Filter) => { return `
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
<span class="cj-table-link open-edit-modal-js max-w-[250px] truncate" data-id="${row.id}">
|
||||||
<span class="cj-table-link max-w-[250px] truncate">
|
|
||||||
${data}
|
${data}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
`}
|
||||||
`}
|
},
|
||||||
},
|
{
|
||||||
{
|
target: 2,
|
||||||
target: 2,
|
data: "value",
|
||||||
data: "value",
|
orderable: true,
|
||||||
orderable: true,
|
searchable: true,
|
||||||
searchable: true,
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string) => { return `
|
render: (data: string) => { return `
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
<div class="px-6 py-4">
|
||||||
<div class="px-6 py-4 max-w-[600px] truncate">
|
|
||||||
<span class="cj-table-text">
|
<span class="cj-table-text">
|
||||||
${data}
|
${data}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
`}
|
||||||
`}
|
},
|
||||||
},
|
{
|
||||||
{
|
target: 3,
|
||||||
target: 3,
|
data: "matching_algorithm",
|
||||||
data: "matching_algorithm",
|
orderable: true,
|
||||||
orderable: true,
|
searchable: true,
|
||||||
searchable: true,
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: string) => {
|
render: (data: string, type: string) => {
|
||||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
if (type !== "display") return data;
|
||||||
const label = $("<span>").addClass("cj-table-text");
|
|
||||||
let description: string;
|
|
||||||
|
|
||||||
switch (data) {
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
case "ANY":
|
const badge = $("<span>").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");
|
||||||
description = "Any Word";
|
|
||||||
break;
|
badge.text(matchingAlgorithms[data]);
|
||||||
case "ALL":
|
|
||||||
description = "All Words";
|
wrapper.append(badge);
|
||||||
break;
|
return wrapper.get(0);
|
||||||
case "EXACT":
|
|
||||||
description = "Exact Match";
|
|
||||||
break;
|
|
||||||
case "REGEX":
|
|
||||||
description = "Regular Expression";
|
|
||||||
break;
|
|
||||||
case "FUZZY":
|
|
||||||
description = "Fuzzy Match";
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 4,
|
||||||
|
data: "is_insensitive",
|
||||||
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: boolean) => {
|
||||||
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
|
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||||
|
|
||||||
label.text(description);
|
if (data) {
|
||||||
wrapper.append(label);
|
badge.text("Case-Insensitive");
|
||||||
return wrapper.get(0);
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||||
}
|
} else {
|
||||||
// render: (data: string) => { return `
|
badge.text("Case-Sensitive");
|
||||||
// <td class="size-px whitespace-nowrap align-top">
|
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||||
// <div class="px-6 py-4">
|
}
|
||||||
// <span class="cj-table-text">
|
|
||||||
// ${data}
|
wrapper.append(badge);
|
||||||
// </span>
|
return wrapper.get(0);
|
||||||
// </div>
|
|
||||||
// </td>
|
|
||||||
// `}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 4,
|
|
||||||
data: "is_insensitive",
|
|
||||||
orderable: true,
|
|
||||||
searchable: true,
|
|
||||||
render: data => {
|
|
||||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
|
||||||
const badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
|
||||||
const label = $("<span>");
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 5,
|
||||||
|
data: "is_whitelist",
|
||||||
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: boolean) => {
|
||||||
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
|
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 text-xs rounded-md");
|
||||||
|
|
||||||
wrapper.append(badge);
|
if (data) {
|
||||||
return wrapper.get(0);
|
badge.text("Whitelist");
|
||||||
}
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||||
},
|
} else {
|
||||||
{
|
badge.text("Blacklist");
|
||||||
target: 5,
|
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||||
data: "is_whitelist",
|
}
|
||||||
orderable: true,
|
|
||||||
searchable: true,
|
wrapper.append(badge);
|
||||||
render: data => {
|
return wrapper.get(0);
|
||||||
const wrapper = $("<div>").addClass("px-6 py-4");
|
|
||||||
const badge = $("<span>").addClass("py-1 px-2 inline-flex items-center text-xs font-medium rounded-full");
|
|
||||||
const label = $("<span>");
|
|
||||||
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
wrapper.append(badge);
|
{
|
||||||
return wrapper.get(0);
|
target: 6,
|
||||||
}
|
data: "created_at",
|
||||||
},
|
orderable: true,
|
||||||
{
|
searchable: false,
|
||||||
target: 6,
|
className: "size-px whitespace-nowrap",
|
||||||
data: "created_at",
|
render: (data: string) => { return `
|
||||||
orderable: true,
|
|
||||||
searchable: false,
|
|
||||||
render: (data: string) => { return `
|
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<span class="cj-table-text">
|
<span class="cj-table-text">
|
||||||
${formatTimestamp(data)}
|
${formatTimestamp(data)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
`}
|
||||||
`}
|
},
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ajaxSettings: AjaxSettings = {
|
const ajaxSettings: AjaxSettings = {
|
||||||
url: `/guild/${1204426362794811453}/filters/api/datatable`,
|
url: `/guild/${guildId}/filters/api/datatable`,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
dataSrc: "data",
|
dataSrc: "data",
|
||||||
@ -202,6 +179,10 @@ const tableOptions: IDataTableOptions = {
|
|||||||
ajax: ajaxSettings,
|
ajax: ajaxSettings,
|
||||||
serverSide: true,
|
serverSide: true,
|
||||||
processing: true,
|
processing: true,
|
||||||
|
select: {
|
||||||
|
style: "multi",
|
||||||
|
selector: "td:first-child input[type='checkbox']"
|
||||||
|
},
|
||||||
columnDefs: columnDefs,
|
columnDefs: columnDefs,
|
||||||
pagingOptions: { pageBtnClasses: "hidden" },
|
pagingOptions: { pageBtnClasses: "hidden" },
|
||||||
rowSelectingOptions: { selectAllSelector: "#selectAllBox" },
|
rowSelectingOptions: { selectAllSelector: "#selectAllBox" },
|
||||||
@ -210,20 +191,60 @@ const tableOptions: IDataTableOptions = {
|
|||||||
emptyTable: emptyTableHtml,
|
emptyTable: emptyTableHtml,
|
||||||
loadingRecords: "Placeholder loading message..."
|
loadingRecords: "Placeholder loading message..."
|
||||||
},
|
},
|
||||||
|
drawCallback: () => HSDropdown.autoInit(),
|
||||||
rowCallback: (row: HTMLTableRowElement) => {
|
rowCallback: (row: HTMLTableRowElement) => {
|
||||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const table: HSDataTable = new HSDataTable(
|
const table = new HSDataTable(
|
||||||
$("#table").get(0),
|
$("#table").get(0) as HTMLElement,
|
||||||
tableOptions
|
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
|
// #endregion
|
||||||
|
|
||||||
// #region Page Size Select
|
// #region Page Size Select
|
||||||
// https://preline.co/plugins/html/advanced-select.html
|
|
||||||
|
|
||||||
(window as any).$hsSelectCollection = [];
|
(window as any).$hsSelectCollection = [];
|
||||||
(window as any)["FloatingUIDOM"] = {
|
(window as any)["FloatingUIDOM"] = {
|
||||||
@ -232,13 +253,19 @@ const table: HSDataTable = new HSDataTable(
|
|||||||
offset: offset
|
offset: offset
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close on click.
|
||||||
|
window.addEventListener('click', (evt) => {
|
||||||
|
const evtTarget = evt.target;
|
||||||
|
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
const pageSelectOptions: ISelectOptions = {
|
const pageSelectOptions: ISelectOptions = {
|
||||||
toggleTag: "<button type=\"button\" aria-expanded=\"false\"></button>",
|
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
||||||
optionTemplate: `
|
optionTemplate: `
|
||||||
<div class=\"flex justify-between items-center w-full\">
|
<div class="flex justify-between items-center w-full">
|
||||||
<span data-title></span>
|
<span data-title></span>
|
||||||
<span class=\"hidden hs-selected:block\">
|
<span class="hidden hs-selected:block">
|
||||||
<svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.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\"><polyline points=\"20 6 9 17 4 12\"/></svg>
|
<svg class="shrink-0 size-3.5 text-blue-600 dark:text-blue-500" xmlns="http:.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"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>`,
|
</div>`,
|
||||||
toggleClasses: "cj-table-paging-select-toggle",
|
toggleClasses: "cj-table-paging-select-toggle",
|
||||||
@ -251,8 +278,134 @@ const pageSelectOptions: ISelectOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeSelect: HSSelect = new HSSelect(
|
const pageSizeSelect: HSSelect = new HSSelect(
|
||||||
$("#selectPageSize-js").get(0),
|
$("#selectPageSize-js").get(0) as HTMLElement,
|
||||||
pageSelectOptions
|
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: '<button type="button" aria-expanded="false"><span data-title></span></button>',
|
||||||
|
optionTemplate: `
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<span data-title></span>
|
||||||
|
<span class="hidden hs-selected:block">
|
||||||
|
<svg class="shrink-0 size-3.5 text-blue-600 dark:text-blue-500" xmlns="http:.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"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>`,
|
||||||
|
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
|
// #endregion
|
@ -22,6 +22,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
|
<button type="button" id="deleteRowsBtn" disabled class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800" href="#">
|
||||||
|
<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="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||||
|
<span>
|
||||||
|
<span class="hidden sm:inline">Delete</span>
|
||||||
|
<span class="rows-selected-count-js zero-empty-js before:content-['('] after:content-[')'] empty:hidden"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="open-edit-modal-js 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">
|
<button type="button" class="open-edit-modal-js 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>
|
<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>
|
<span>
|
||||||
@ -38,83 +45,62 @@
|
|||||||
<table class="cj-table">
|
<table class="cj-table">
|
||||||
<thead class="cj-thead">
|
<thead class="cj-thead">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
<th scope="col" class="cj-table-header --exclude-from-ordering">
|
||||||
<label for="selectAllBox" class="">
|
<label for="selectAllBox" class="flex">
|
||||||
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
||||||
<span class="sr-only">Checkbox</span>
|
<span class="sr-only">Checkbox</span>
|
||||||
</label>
|
</label>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="name" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="name" class="cj-table-header">
|
||||||
<div class="flex justify-between item-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Name</span>
|
||||||
Name
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="url" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="value" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Value</span>
|
||||||
Value
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="url" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="matching_algorithm" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Algorithm</span>
|
||||||
Algorithm
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="url" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="is_insensitive" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Case Sensitivity</span>
|
||||||
Case-Sensitive
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="url" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="is_whitelist" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Filter Type</span>
|
||||||
Type
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="created_at" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="created_at" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
<span>Created at</span>
|
||||||
Created at
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<svg class="size-3.5 ms-1 -me-0.5 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">
|
|
||||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
|
||||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -156,17 +142,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1" aria-labelledby="hs-scale-animation-modal-label">
|
<div id="editModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1">
|
||||||
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-4xl lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-lg lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||||
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-bold text-gray-800 dark:text-neutral-200">Filters</h2>
|
<h2 class="text-xl font-bold text-gray-800 dark:text-neutral-200">Filter</h2>
|
||||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
Manage your filters.
|
Manage your filters to organise the content in your feeds.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form id="editForm" novalidate class="group grid sm:grid-cols-2 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
<form id="editForm" novalidate class="group grid grid-cols-1 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
||||||
placeholder form content
|
|
||||||
|
<div>
|
||||||
|
<label for="formName" class="text-input-label">Name</label>
|
||||||
|
<input type="text" id="formName" name="name" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||||
|
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||||
|
Human-readable name for this entry.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||||
|
Please enter a name.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="formUrl" class="text-input-label">Value</label>
|
||||||
|
<input type="text" id="formValue" name="value" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||||
|
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||||
|
The value to match against feed content.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||||
|
Please enter a value.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="formAlgorithm" class="text-input-label">Matching Algorithm</label>
|
||||||
|
<select name="matching_algorithm" id="formAlgorithm" class="peer --prevent-on-load-init" required>
|
||||||
|
<option value="">Choose</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-input-help block peer-has-invalid:group-[.submitted]:hidden">
|
||||||
|
How the filter value will be matched against feed content.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-red-500 hidden peer-has-invalid:group-[.submitted]:block">
|
||||||
|
Please select an algorithm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="formInsensitive" class="flex gap-4">
|
||||||
|
<input type="checkbox" id="formInsensitive" name="is_insensitive" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="block text-sm dark:text-neutral-400">Case-Insensitive</span>
|
||||||
|
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
|
By default the filter value will be case-sensitive.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="formWhitelist" class="flex gap-4">
|
||||||
|
<input type="checkbox" id="formWhitelist" name="is_whitelist" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="block text-sm dark:text-neutral-400">Is Whitelist?</span>
|
||||||
|
<span class="block text-sm text-gray-500 dark:text-neutral-500">
|
||||||
|
By default filters will blacklist non-matching content.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
<div class="flex items-center gap-x-2 mt-8">
|
<div class="flex items-center gap-x-2 mt-8">
|
||||||
<button type="button" class="me-auto py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
<button type="button" class="me-auto py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||||
@ -183,4 +224,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var guildId = "<%- guild.id %>";
|
||||||
|
var matchingAlgorithms = JSON.parse(`<%- JSON.stringify( matchingAlgorithms ) %> `);
|
||||||
|
</script>
|
||||||
<% block("scripts").append('<script src="/public/generated/js/guild/filters.js"></script>'); %>
|
<% block("scripts").append('<script src="/public/generated/js/guild/filters.js"></script>'); %>
|
@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { client as bot } from "@bot/bot";
|
import { client as bot } from "@bot/bot";
|
||||||
|
import prisma from "generated/prisma";
|
||||||
|
|
||||||
export const get = async (request: Request, response: Response) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
const guildId = request.params.guildId;
|
const guildId = request.params.guildId;
|
||||||
@ -10,9 +11,18 @@ export const get = async (request: Request, response: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchingAlgorithms: Record<keyof typeof prisma.MatchingAlgorithms, string> = {
|
||||||
|
ANY: "Any Word",
|
||||||
|
ALL: "All Words",
|
||||||
|
EXACT: "Exact Match",
|
||||||
|
REGEX: "Regular Expression",
|
||||||
|
FUZZY: "Fuzzy Match"
|
||||||
|
};
|
||||||
|
|
||||||
response.render("guild/filters", {
|
response.render("guild/filters", {
|
||||||
title: `${guild.name} - Relay`,
|
title: `${guild.name} - Relay`,
|
||||||
guild: guild
|
guild: guild,
|
||||||
|
matchingAlgorithms: matchingAlgorithms
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user