530 lines
19 KiB
TypeScript
530 lines
19 KiB
TypeScript
import { formatTimestamp, genHexString } from "../main";
|
|
import HSDropdown from "preline/dist/dropdown";
|
|
import HSSelect, { ISelectOptions } from "preline/dist/select";
|
|
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
|
|
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
|
|
import { Api, AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
|
|
import "datatables.net-select-dt";
|
|
import prisma from "../../../../../generated/prisma";
|
|
|
|
declare let guildId: string;
|
|
declare const textMutators: { [key: string]: string };
|
|
|
|
// #region DataTable
|
|
|
|
const emptyTableHtml: string = `
|
|
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
|
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
|
<svg class="shrink-0 size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42"></path></svg>
|
|
</div>
|
|
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
|
No results found
|
|
</h2>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
|
Refine your search or create a new style.
|
|
Alternatively, use a template to deploy a ready-made style.
|
|
</p>
|
|
|
|
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
|
<button type="button" class="open-edit-modal-js py-2 px-3 inline-flex justify-center 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>
|
|
Create a style
|
|
</button>
|
|
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex justify-center 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">
|
|
Use a Template
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const columnDefs: ConfigColumnDefs[] = [
|
|
// Select checkbox column
|
|
{
|
|
target: 0,
|
|
orderable: false,
|
|
searchable: false,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (_data: unknown, _type: unknown, row: prisma.MessageStyle) => { return `
|
|
<div class="ps-6 py-4">
|
|
<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="">
|
|
<span class="sr-only">Select Row</span>
|
|
</label>
|
|
</div>
|
|
`}
|
|
},
|
|
{
|
|
target: 1,
|
|
data: "name",
|
|
orderable: true,
|
|
searchable: true,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (data: string, _type: string, row: prisma.Feed) => { return `
|
|
<span class="cj-table-link open-edit-modal-js max-w-[250px] truncate" data-id="${row.id}">
|
|
${data}
|
|
</span>
|
|
`}
|
|
},
|
|
{
|
|
target: 2,
|
|
data: "colour",
|
|
orderable: true,
|
|
searchable: true,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (data: string) => { return `
|
|
<div class="px-6 py-4">
|
|
<span class="cj-table-text">
|
|
${data}
|
|
</span>
|
|
</div>
|
|
`}
|
|
},
|
|
{
|
|
target: 3,
|
|
data: "title_mutator",
|
|
orderable: true,
|
|
searchable: true,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (data: string, type: string) => {
|
|
if (type !== "display") return data;
|
|
if (!data) return "";
|
|
|
|
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 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(textMutators[data]);
|
|
|
|
wrapper.append(badge);
|
|
return wrapper.get(0);
|
|
}
|
|
},
|
|
{
|
|
target: 4,
|
|
data: "description_mutator",
|
|
orderable: true,
|
|
searchable: true,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (data: string, type: string) => {
|
|
if (type !== "display") return data;
|
|
if (!data) return "";
|
|
|
|
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 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(textMutators[data]);
|
|
|
|
wrapper.append(badge);
|
|
return wrapper.get(0);
|
|
}
|
|
},
|
|
{
|
|
target: 5,
|
|
data: "show_author",
|
|
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");
|
|
|
|
if (data) {
|
|
badge.text("Show");
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
} else {
|
|
badge.text("Hide");
|
|
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: 6,
|
|
data: "show_image",
|
|
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");
|
|
|
|
if (data) {
|
|
badge.text("Show");
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
} else {
|
|
badge.text("Hide");
|
|
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: 7,
|
|
data: "show_thumbnail",
|
|
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");
|
|
|
|
if (data) {
|
|
badge.text("Show");
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
} else {
|
|
badge.text("Hide");
|
|
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: 8,
|
|
data: "show_footer",
|
|
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");
|
|
|
|
if (data) {
|
|
badge.text("Show");
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
} else {
|
|
badge.text("Hide");
|
|
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: 9,
|
|
data: "show_timestamp",
|
|
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");
|
|
|
|
if (data) {
|
|
badge.text("Show");
|
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
|
} else {
|
|
badge.text("Hide");
|
|
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: 10,
|
|
data: "created_at",
|
|
orderable: true,
|
|
searchable: false,
|
|
className: "size-px whitespace-nowrap",
|
|
render: (data: string) => { return `
|
|
<div class="px-6 py-4">
|
|
<span class="cj-table-text">
|
|
${formatTimestamp(data)}
|
|
</span>
|
|
</div>
|
|
`}
|
|
},
|
|
];
|
|
|
|
const ajaxSettings: AjaxSettings = {
|
|
url: `/guild/${guildId}/styles/api/datatable`,
|
|
type: "POST",
|
|
contentType: "application/json",
|
|
dataSrc: "data",
|
|
data: (data: unknown) => {
|
|
if (data === undefined) return;
|
|
// TODO,
|
|
return JSON.stringify(data);
|
|
}
|
|
};
|
|
|
|
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" },
|
|
language: {
|
|
zeroRecords: emptyTableHtml,
|
|
emptyTable: emptyTableHtml,
|
|
loadingRecords: "Placeholder loading message..."
|
|
},
|
|
drawCallback: () => HSDropdown.autoInit(),
|
|
rowCallback: (row: HTMLTableRowElement) => {
|
|
$(row).addClass("bg-white dark:bg-neutral-900");
|
|
}
|
|
};
|
|
|
|
let table: HSDataTable;
|
|
|
|
window.addEventListener("preline:ready", () => {
|
|
const tableEl = $("#table").get(0);
|
|
|
|
if (HSDataTable.getInstance(tableEl, true)) return;
|
|
|
|
table = new HSDataTable(tableEl, tableOptions);
|
|
|
|
(table as any).dataTable
|
|
.on("select", onTableSelectChange)
|
|
.on("deselect", onTableSelectChange)
|
|
.on("draw", onTableSelectChange);
|
|
});
|
|
|
|
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();
|
|
};
|
|
|
|
$("#selectAllBox").on("change", function() {
|
|
const dt: Api = (table as any).dataTable;
|
|
(this as HTMLInputElement).checked
|
|
? dt.rows().select()
|
|
: dt.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.MessageStyle) => row.id);
|
|
|
|
await $.ajax({
|
|
url: `/guild/${guildId}/styles/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 Table Paging Select
|
|
|
|
const pageSelectOptions: ISelectOptions = {
|
|
toggleTag: '<button type="button" aria-expanded="false"></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-table-paging-select-toggle",
|
|
optionClasses: "cj-table-paging-select-option",
|
|
dropdownClasses: "cj-table-paging-select-dropdown",
|
|
dropdownSpace: 10,
|
|
dropdownScope: "parent",
|
|
dropdownPlacement: "top",
|
|
dropdownVerticalFixedPlacement: null
|
|
};
|
|
|
|
window.addEventListener("preline:ready", () => {
|
|
const selectEl = $("#selectPageSize-js").get(0);
|
|
if (!HSSelect.getInstance(selectEl, true)) {
|
|
new HSSelect(selectEl, pageSelectOptions);
|
|
}
|
|
});
|
|
|
|
// #endregion
|
|
|
|
// #region Edit Modal
|
|
|
|
const closeEditModal = () => { editModal.close() };
|
|
|
|
const openEditModal = async (id: number | undefined) => {
|
|
$("#editForm").removeClass("submitted");
|
|
editModal.open();
|
|
|
|
id === undefined
|
|
? clearEditModalData()
|
|
: loadEditModalData(id);
|
|
};
|
|
|
|
$(document).on("click", ".open-edit-modal-js", async event => {
|
|
await openEditModal($(event.target).data("id"));
|
|
});
|
|
|
|
const editModalOptions: IOverlayOptions = {};
|
|
|
|
let editModal: HSOverlay;
|
|
|
|
window.addEventListener("preline:ready", () => {
|
|
const modalEl = $("#editModal").get(0);
|
|
if (!HSOverlay.getInstance(modalEl, true)) {
|
|
editModal = new HSOverlay(modalEl, editModalOptions);
|
|
}
|
|
});
|
|
|
|
// #endregion
|
|
|
|
// #region Edit Form
|
|
|
|
const clearEditModalData = () => {
|
|
$(editModal.el).removeData("id");
|
|
|
|
$("#formName").val("");
|
|
updateColourInput("#5865F2");
|
|
|
|
titleMutatorSelect.setValue("");
|
|
descriptionMutatorSelect.setValue("");
|
|
|
|
$("#formShowAuthor").prop("checked", true);
|
|
$("#formShowImage").prop("checked", true);
|
|
$("#formShowThumbnail").prop("checked", true);
|
|
$("#formShowFooter").prop("checked", true);
|
|
$("#formShowTimestamp").prop("checked", true);
|
|
};
|
|
|
|
const loadEditModalData = async (id: number) => {
|
|
const style: prisma.MessageStyle = await $.ajax({
|
|
url: `/guild/${guildId}/styles/api?id=${id}`,
|
|
method: "get"
|
|
});
|
|
|
|
$(editModal.el).data("id", style.id);
|
|
|
|
$("#formName").val(style.name);
|
|
updateColourInput("#5865F2");
|
|
|
|
titleMutatorSelect.setValue(style.title_mutator || "");
|
|
descriptionMutatorSelect.setValue(style.description_mutator || "");
|
|
|
|
$("#formShowAuthor").prop("checked", style.show_author);
|
|
$("#formShowImage").prop("checked", style.show_image);
|
|
$("#formShowThumbnail").prop("checked", style.show_thumbnail);
|
|
$("#formShowFooter").prop("checked", style.show_footer);
|
|
$("#formShowTimestamp").prop("checked", style.show_timestamp);
|
|
};
|
|
|
|
$("#editForm").on("submit", async event => {
|
|
event.preventDefault();
|
|
|
|
const form = $(event.target).get(0) as HTMLFormElement;
|
|
$(form).addClass("submitted");
|
|
|
|
const validity = form.checkValidity();
|
|
if (!validity) {
|
|
console.debug(`Submit form invalid: ${validity}`);
|
|
return;
|
|
};
|
|
|
|
let method = "post";
|
|
const data = $(event.target).serializeArray();
|
|
|
|
// If 'id' has a value, we are patching an existing entry
|
|
const id: number | undefined = $(editModal.el).data("id");
|
|
if (id !== undefined) {
|
|
data.push({ name: "id", value: `${id}` });
|
|
method = "patch";
|
|
}
|
|
|
|
await $.ajax({
|
|
url: `/guild/${guildId}/styles/api`,
|
|
dataType: "json",
|
|
method: method,
|
|
data: data,
|
|
success: () => {
|
|
(table as any).dataTable.draw()
|
|
closeEditModal();
|
|
},
|
|
error: error => {
|
|
alert(JSON.stringify(error, null, 4));
|
|
}
|
|
});
|
|
});
|
|
|
|
const mutatorSelectOptions: 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
|
|
};
|
|
|
|
let titleMutatorSelect: HSSelect;
|
|
let descriptionMutatorSelect: HSSelect;
|
|
|
|
window.addEventListener("preline:ready", () => {
|
|
const exists = (element: HTMLElement) => HSSelect.getInstance(element, true);
|
|
|
|
const titleEl = $("#formTitleMutator").get(0);
|
|
const descEl = $("#formDescriptionMutator").get(0);
|
|
|
|
if (exists(titleEl) || exists(descEl)) return;
|
|
|
|
titleMutatorSelect = new HSSelect(titleEl, mutatorSelectOptions);
|
|
titleMutatorSelect.addOption({ title: "None", val: "" });
|
|
|
|
descriptionMutatorSelect = new HSSelect(descEl, mutatorSelectOptions);
|
|
descriptionMutatorSelect.addOption({ title: "None", val: "" });
|
|
|
|
Object.entries(textMutators).forEach(([key, description]) => {
|
|
const option = {title: description, val: key};
|
|
titleMutatorSelect.addOption(option);
|
|
descriptionMutatorSelect.addOption(option);
|
|
});
|
|
});
|
|
|
|
const colourPicker = $("#formColour") as JQuery<HTMLInputElement>;
|
|
const colourTextInput = $("#formColourInput") as JQuery<HTMLInputElement>;
|
|
const colourRandomBtn = $("#formColourRandomBtn") as JQuery<HTMLButtonElement>;
|
|
|
|
const updateColourInput = (value: string) => {
|
|
value = "#" + value.replace(/[^A-F0-9]/gi, '')
|
|
.toUpperCase()
|
|
.slice(0, 6)
|
|
.padEnd(6, "0");
|
|
|
|
colourPicker.val(value);
|
|
colourTextInput.val(value);
|
|
};
|
|
|
|
colourPicker.on("change", _ => updateColourInput(colourPicker.val()));
|
|
colourTextInput.on("change", _ => updateColourInput(colourTextInput.val()));
|
|
colourRandomBtn.on("click", _ => updateColourInput(genHexString(6)));
|
|
|
|
|
|
// #endregion
|