Merge branch 'master' of https://gitea.cor.bz/corbz/relay
Some checks failed
Build / build (push) Failing after 36s

This commit is contained in:
Corban-Lee Jones 2025-05-13 10:14:18 +01:00
commit 851467ab90
11 changed files with 411 additions and 1027 deletions

View File

@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.1.5](https://gitea.cor.bz/corbz/relay/compare/v0.1.4...v0.1.5) (2025-05-09)
### Features
* added styles page table and edit modal ([be03788](https://gitea.cor.bz/corbz/relay/commit/be03788cfcd6148ae03537425581aefecbc67734))
* **api:** added message style api endpoints ([a1bd362](https://gitea.cor.bz/corbz/relay/commit/a1bd362799b640647d6d536c61e5037fcb938fab))
* **api:** Functional API endpoints for filters ([e58d734](https://gitea.cor.bz/corbz/relay/commit/e58d7343b163e56eaeca553a560ff7c112e962df))
* colour picker on message style edit modal ([e9807ee](https://gitea.cor.bz/corbz/relay/commit/e9807ee6f6f0066e0351b58a1e53653af9993874))
* completed filter table and edit modal ([73aed35](https://gitea.cor.bz/corbz/relay/commit/73aed35ce04180e7adc9043bdf9ab2193a9e4d74))
* create and update feeds with filters ([b528153](https://gitea.cor.bz/corbz/relay/commit/b528153113be2773dbe678f3414ffcb20b5db440))
* css shorthands for generic select toggle ([fa42eb3](https://gitea.cor.bz/corbz/relay/commit/fa42eb355198bd86ee45c578669f6152df7d191e))
* **db:** connect message style to feed ([6356bb1](https://gitea.cor.bz/corbz/relay/commit/6356bb1d063b2a4e1d38bf2f90bddf607416ee0e))
* delete selected feed rows ([2589cab](https://gitea.cor.bz/corbz/relay/commit/2589cabec6356179e18f2c6ebb032de474e4ebe9))
* filter selection and table render for feed view ([e935d80](https://gitea.cor.bz/corbz/relay/commit/e935d801e6e2e5910909362098da5ae38f42c6b7))
* migrate from sqlite to postgresql, and add message_style model ([6d1f4e6](https://gitea.cor.bz/corbz/relay/commit/6d1f4e6f7f6529d7bcdbb6db85923a7537320d1e))
* patch existing feeds ([8477285](https://gitea.cor.bz/corbz/relay/commit/84772852e3550bc88b33fb8fa05d173ba40cfb0d))
* publish threshold field on Feed model ([6761a91](https://gitea.cor.bz/corbz/relay/commit/6761a9163bf5e3aef9cdb99b0dbaa70eef43d72d))
* select message style on feed ([d5af04c](https://gitea.cor.bz/corbz/relay/commit/d5af04c317ef58943ce3eefa26d79ca58e3a04d0))
### Bug Fixes
* **api:** expected ids to be integers, not strings ([00a4c74](https://gitea.cor.bz/corbz/relay/commit/00a4c749f0f644905e7c878ddd61404616371327))
* **api:** patching feeds would duplicate channels [#4](https://gitea.cor.bz/corbz/relay/issues/4) ([e4ab506](https://gitea.cor.bz/corbz/relay/commit/e4ab506abe68a4eecb1e91d940f4e367d164a666))
* **api:** style mutators causing errors if blank ([99a59c6](https://gitea.cor.bz/corbz/relay/commit/99a59c61e7b8e73b7b2207eb45c7543f6a3f9f44))
* channels select dropdown height breaking on smaller screens ([2a88e1c](https://gitea.cor.bz/corbz/relay/commit/2a88e1c18459dcbb3b49828769821dba9ffe73e4))
* **database:** cascade channels on feed delete ([79e331b](https://gitea.cor.bz/corbz/relay/commit/79e331bdb9629f3d851bae2c57c16be1e2913cb3))
* datatable count including entries from other guilds ([6b6af17](https://gitea.cor.bz/corbz/relay/commit/6b6af177318fc1a6c67face8c647eb6d66980d03))
* error when searching filters (algorithm isn't searchable via API) ([3ac33dc](https://gitea.cor.bz/corbz/relay/commit/3ac33dc00a364f2c62a8855b902f1680b7f05781))
* ID not being reset when creating new feeds, causing unintentional edits over existing feeds ([a0d2711](https://gitea.cor.bz/corbz/relay/commit/a0d2711a510ee781460f6f7854106f8bb0701950))
* incorrect 'for' value being used on the editModal's 'value' label. ([2d8a26f](https://gitea.cor.bz/corbz/relay/commit/2d8a26f39269dfc517a72b1012a4f9f86061745f))
* mistakenly broken class on the style table delete button ([cfde210](https://gitea.cor.bz/corbz/relay/commit/cfde210a394c28eedabeb3a4f515c4b09b1bd42e))
* parse publish threshold on post/patch ([816da70](https://gitea.cor.bz/corbz/relay/commit/816da70229218684cf699ca7c10bf435471dc6e7))
### [0.1.4](https://gitea.cor.bz/corbz/relay/compare/v0.1.3...v0.1.4) (2025-04-30)

View File

@ -1,6 +1,6 @@
{
"name": "relay",
"version": "0.1.4",
"version": "0.1.5",
"main": "index.js",
"scripts": {
"start": "node ./dist/app.js",
@ -50,11 +50,6 @@
},
"dependencies": {
"@floating-ui/dom": "^1.6.13",
"@preline/datatable": "^3.0.0",
"@preline/datepicker": "^3.0.1",
"@preline/dropdown": "^3.0.1",
"@preline/overlay": "^3.0.0",
"@preline/select": "^3.0.0",
"@prisma/client": "^6.6.0",
"@tailwindcss/forms": "^0.5.10",
"datatables.net-dt": "^2.2.2",
@ -70,7 +65,6 @@
"lodash": "^4.17.21",
"nouislider": "^15.8.1",
"preline": "^3.0.1",
"sqlite3": "^5.1.7",
"tsconfig-paths": "^4.2.0",
"vanilla-calendar-pro": "^3.0.4",
"winston": "^3.17.0"

View File

@ -1,45 +1,17 @@
import $ from "jquery";
import HSDropdown from "@preline/dropdown";
import HSOverlay, { IOverlayOptions } from "@preline/overlay";
import HSSelect, { ISelectOptions } from "@preline/select";
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
import DataTable, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
import { formatTimestamp, verifyChannels } from "../../../src/ts/main";
import { formatTimestamp, verifyChannels } 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 { TextChannel } from "discord.js";
import "datatables.net-select-dt"
import prisma from "../../../../../generated/prisma";
import HSDatepicker, { ICustomDatepickerOptions } from "@preline/datepicker";
// import "preline";
// import _ from "lodash";
// import noUiSlider from "nouislider";
// import "datatables.net";
// import "dropzone/dist/dropzone-min.js";
// import * as VanillaCalendarPro from "vanilla-calendar-pro";
// // Preline requirements
// window._ = _;
// window.$ = $;
// window.jQuery = $;
// window.DataTable = $.fn.dataTable;
// window.noUiSlider = noUiSlider;
// window.VanillaCalendarPro = VanillaCalendarPro;
// window.HSStaticMethods.autoInit();
// // document.addEventListener("DOMContentLoaded", () => {
// // if (window.HSStaticMethods && typeof window.HSStaticMethods.autoInit === "function") {
// // } else {
// // console.warn("Preline is not available on window.HSStaticMethods.");
// // }
// // });
declare let guildId: string;
declare let channels: Array<any>;
// #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">
@ -67,8 +39,7 @@ const emptyTableHtml: string = `
`;
const columnDefs: ConfigColumnDefs[] = [
// Select checkbox column
{
{ // Select checkbox column
target: 0,
orderable: false,
searchable: false,
@ -292,10 +263,20 @@ const tableOptions: IDataTableOptions = {
}
};
const table: HSDataTable = new HSDataTable(
$("#table").get(0) as HTMLElement,
tableOptions
);
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();
@ -306,15 +287,11 @@ const onTableSelectChange = () => {
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
};
(table as any).dataTable
.on("select", onTableSelectChange)
.on("deselect", onTableSelectChange)
.on("draw", onTableSelectChange);
$("#selectAllBox").on("change", function() {
const dt: Api = (table as any).dataTable;
(this as HTMLInputElement).checked
? (table as any).dataTable.rows().select()
: (table as any).dataTable.rows().deselect();
? dt.rows().select()
: dt.rows().deselect();
});
$("#deleteRowsBtn").on("click", async () => {
@ -339,21 +316,7 @@ $("#deleteRowsBtn").on("click", async () => {
// #endregion
// #region Page Size Select
// https://preline.co/plugins/html/advanced-select.html
(window as any)["FloatingUIDOM"] = {
computePosition: computePosition,
autoUpdate: autoUpdate,
offset: offset
};
// Close on click.
window.addEventListener('click', (evt) => {
const evtTarget = evt.target;
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
});
// #region Table Paging Select
const pageSelectOptions: ISelectOptions = {
toggleTag: '<button type="button" aria-expanded="false"></button>',
@ -373,31 +336,50 @@ const pageSelectOptions: ISelectOptions = {
dropdownVerticalFixedPlacement: null
};
const pageSizeSelect: HSSelect = new HSSelect(
$("#selectPageSize-js").get(0) as HTMLElement,
pageSelectOptions
);
window.addEventListener("preline:ready", () => {
const selectEl = $("#selectPageSize-js").get(0);
if (!HSSelect.getInstance(selectEl, true)) {
new HSSelect(selectEl, pageSelectOptions);
}
});
// #endregion
// #region Edit Modal
(window as any).$hsOverlayCollection = [];
const closeEditModal = () => { editModal.close() };
const editModalOptions: IOverlayOptions = {};
const openEditModal = async (id: number | undefined) => {
$("#editForm").removeClass("submitted");
editModal.open();
const editModal: HSOverlay = new HSOverlay(
$("#editModal").get(0) as HTMLElement,
editModalOptions
);
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
interface ExpandedFeed extends prisma.Feed {
channels: prisma.Channel[],
filters: prisma.Feed[]
channels: prisma.Channel[];
filters: prisma.Feed[];
}
const clearEditModalData = () => {
@ -405,6 +387,7 @@ const clearEditModalData = () => {
$("#formName").val("");
$("#formUrl").val("");
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
$("#formActive").prop("checked", true);
channelSelect.setValue([]);
filterSelect.setValue([]);
@ -419,34 +402,56 @@ const loadEditModalData = async (id: number) => {
$(editModal.el).data("id", feed.id);
const publishedThreshold = new Date(feed.published_threshold as unknown as string)
$("#formName").val(feed.name);
$("#formUrl").val(feed.url);
$("#formPublishedThreshold").val(publishedThreshold.toISOString().slice(0, 16));
$("#formActive").prop("checked", feed.active);
channelSelect.setValue(feed.channels.map(channel => channel.channel_id));
filterSelect.setValue(feed.filters.map(filter => `${filter.id}`));
styleSelect.setValue(`${feed.message_style_id}`);
}
const openEditModal = async (id: number | undefined) => {
// ISSUE:
// The calculation with `channelSelect.setValue([])` assumes components are visible
// when determining the width of the input placeholder 'Select option...'. This
// requires the modal to be opened before running the calculation, which could be
// bad.
$("#editForm").removeClass("submitted");
editModal.open();
id === undefined
? clearEditModalData()
: loadEditModalData(id);
};
const closeEditModal = () => {
editModal.close();
};
$("#editForm").on("submit", async event => {
event.preventDefault();
const channelsSelectOptions: ISelectOptions = {
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}/feeds/api`,
dataType: "json",
method: method,
data: data,
success: () => {
(table as any).dataTable.draw()
closeEditModal();
},
error: error => {
alert(JSON.stringify(error, null, 4));
}
});
});
const channelSelectOptions: ISelectOptions = {
placeholder: "Select option....",
mode: "tags",
@ -487,11 +492,6 @@ const channelsSelectOptions: ISelectOptions = {
searchNoResultClasses: "cj-tag-select-search-no-results",
};
const channelSelect = new HSSelect(
$("#formChannels").get(0) as HTMLElement,
channelsSelectOptions
);
const filterSelectOptions: ISelectOptions = {
placeholder: "Select option....",
mode: "tags",
@ -549,11 +549,6 @@ const filterSelectOptions: ISelectOptions = {
};
const filterSelect = new HSSelect(
$("#formFilters").get(0),
filterSelectOptions
);
const styleSelectOptions: ISelectOptions = {
placeholder: "Select option...",
@ -588,56 +583,40 @@ const styleSelectOptions: ISelectOptions = {
optionAllowEmptyOption: true
};
const styleSelect = new HSSelect(
$("#formMessageStyle").get(0),
styleSelectOptions
);
let channelSelect: HSSelect;
let filterSelect: HSSelect;
let styleSelect: HSSelect;
const publishedThresholdOptions: ICustomDatepickerOptions = {
type: "default",
dateMax: "2050-00-00",
mode: "default",
layouts: {
window.addEventListener("preline:ready", () => {
const exists = (element: HTMLElement) => HSSelect.getInstance(element, true);
},
inputModeOptions: { itemsSeparator: " / " },
templates: {
arrowPrev: '<button data-vc-arrow="prev"><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"></path></svg></button>',
arrowNext: '<button data-vc-arrow="next"><svg class="shrink-0 size-4" 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"></path></svg></button>'
}
};
const channelEl = $("#formChannels").get(0);
const filterEl = $("#formFilters").get(0);
const styleEl = $("#formMessageStyle").get(0);
$("#editForm").on("submit", async event => {
event.preventDefault();
if (exists(channelEl) || exists(filterEl) || exists(styleEl)) return;
const form = $(event.target).get(0) as HTMLFormElement;
$(form).addClass("submitted");
channelSelect = new HSSelect(channelEl, channelSelectOptions);
filterSelect = new HSSelect(filterEl, filterSelectOptions);
styleSelect = new HSSelect(styleEl, styleSelectOptions);
if (!form.checkValidity()) 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}/feeds/api`,
dataType: "json",
method: method,
data: data,
success: () => {
(table as any).dataTable.draw() // is this okay? dataTable is private, but there is no other method I know of to redraw...
closeEditModal();
},
error: error => {
alert(JSON.stringify(error, null, 4));
}
});
// Add options to the channel select
channels.forEach((channel: TextChannel) => {
channelSelect.addOption({
title: channel.name,
val: channel.id,
options: {
description: channel.id,
icon: `
<svg class="shrink-0 size-[16px]" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="9" x2="20" y2="9"></line>
<line x1="4" y1="15" x2="20" y2="15"></line>
<line x1="10" y1="3" x2="8" y2="21"></line>
<line x1="16" y1="3" x2="14" y2="21"></line>
</svg>` // hashtag icon
}
});
})
});
// #endregion

View File

@ -1,12 +1,10 @@
import $ from "jquery";
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, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
import { formatTimestamp } 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;
@ -14,9 +12,6 @@ declare const matchingAlgorithms: { [key: string]: string };
// #region DataTable
(window as any).DataTable = DataTable;
(window as any).$hsDataTableCollection = [];
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">
@ -43,8 +38,7 @@ const emptyTableHtml: string = `
`;
const columnDefs: ConfigColumnDefs[] = [
// Select checkbox column
{
{ // Select checkbox column
target: 0,
orderable: false,
searchable: false,
@ -197,10 +191,20 @@ const tableOptions: IDataTableOptions = {
}
};
const table = new HSDataTable(
$("#table").get(0) as HTMLElement,
tableOptions
)
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();
@ -211,15 +215,11 @@ const onTableSelectChange = () => {
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
};
(table as any).dataTable
.on("select", onTableSelectChange)
.on("deselect", onTableSelectChange)
.on("draw", onTableSelectChange);
$("#selectAllBox").on("change", function() {
const dt: Api = (table as any).dataTable;
(this as HTMLInputElement).checked
? (table as any).dataTable.rows().select()
: (table as any).dataTable.rows().deselect();
? dt.rows().select()
: dt.rows().deselect();
});
$("#deleteRowsBtn").on("click", async () => {
@ -244,20 +244,7 @@ $("#deleteRowsBtn").on("click", async () => {
// #endregion
// #region Page Size Select
(window as any).$hsSelectCollection = [];
(window as any)["FloatingUIDOM"] = {
computePosition: computePosition,
autoUpdate: autoUpdate,
offset: offset
};
// Close on click.
window.addEventListener('click', (evt) => {
const evtTarget = evt.target;
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
});
// #region Table Paging Select
const pageSelectOptions: ISelectOptions = {
toggleTag: '<button type="button" aria-expanded="false"></button>',
@ -270,35 +257,52 @@ const pageSelectOptions: ISelectOptions = {
</div>`,
toggleClasses: "cj-table-paging-select-toggle",
optionClasses: "cj-table-paging-select-option",
dropdownClasses: `cj-table-paging-select-dropdown`,
dropdownClasses: "cj-table-paging-select-dropdown",
dropdownSpace: 10,
dropdownScope: "parent",
dropdownPlacement: "top",
dropdownVerticalFixedPlacement: null
};
const pageSizeSelect: HSSelect = new HSSelect(
$("#selectPageSize-js").get(0) as HTMLElement,
pageSelectOptions
);
// #endregion
window.addEventListener("preline:ready", () => {
const selectEl = $("#selectPageSize-js").get(0);
if (!HSSelect.getInstance(selectEl, true)) {
new HSSelect(selectEl, pageSelectOptions);
}
});
// #region Edit Modal
(window as any).$hsOverlayCollection = [];
const closeEditModal = () => { editModal.close() };
const editModalOptions: IOverlayOptions = {};
const openEditModal = async (id: number | undefined) => {
$("#editForm").removeClass("submitted");
editModal.open();
const editModal: HSOverlay = new HSOverlay(
$("#editModal").get(0) as HTMLElement,
editModalOptions
);
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");
@ -323,23 +327,45 @@ const loadEditModalData = async (id: number) => {
$("#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();
};
$("#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}/filters/api`,
dataType: "json",
method: method,
data: data,
success: () => {
(table as any).dataTable.draw();
closeEditModal();
},
error: error => {
alert(JSON.stringify(error, null, 4));
}
});
});
const algorithmSelectOptions: ISelectOptions = {
toggleTag: '<button type="button" aria-expanded="false"><span data-title></span></button>',
@ -360,51 +386,20 @@ const algorithmSelectOptions: ISelectOptions = {
dropdownVerticalFixedPlacement: null
};
const algorithmSelect = new HSSelect(
$("#formAlgorithm").get(0),
algorithmSelectOptions
);
let algorithmSelect: HSSelect;
// Add options to algorithm select
Object.entries(matchingAlgorithms).forEach(([key, description]) => {
algorithmSelect.addOption({
title: description,
val: key
} as ISingleOption)
})
window.addEventListener("preline:ready", () => {
const algorithmEl = $("#formAlgorithm").get(0)
$("#editForm").on("submit", async event => {
event.preventDefault();
if (HSSelect.getInstance(algorithmEl, true)) return;
const form = $(event.target).get(0) as HTMLFormElement;
$(form).addClass("submitted");
algorithmSelect = new HSSelect(algorithmEl, algorithmSelectOptions);
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));
}
Object.entries(matchingAlgorithms).forEach(([key, description]) => {
algorithmSelect.addOption({
title: description,
val: key
});
});
});

View File

@ -1,23 +1,16 @@
import $ from "jquery";
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 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, { Api, ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
import { formatTimestamp, genHexString } from "../../../src/ts/main";
import prisma from "../../../../../generated/prisma";
declare let guildId: string;
declare const textMutators: { [key: string]: string };
// #region DataTable
//
// Fix dependency bugs with preline
(window as any).DataTable = DataTable;
(window as any).$hsDataTableCollection = [];
const emptyTableHtml: string = `
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
@ -44,7 +37,6 @@ const emptyTableHtml: string = `
</div>
`;
const columnDefs: ConfigColumnDefs[] = [
// Select checkbox column
{
@ -79,19 +71,25 @@ const columnDefs: ConfigColumnDefs[] = [
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>
`}
render: (data: string, type: string) => {
if (type !== "display") return data;
const wrapper = $("<div>").addClass("flex px-6 py-4");
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap border rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 overflow-hidden");
const colour = $("<span>").addClass("size-6 shrink-0").css("background-color", data);
const label = $("<span>").addClass("py-1 px-2.5 text-xs text-gray-800 dark:text-neutral-200");
label.text(data);
badge.append(colour).append(label);
wrapper.append(badge);
return wrapper.get(0);
}
},
{
target: 3,
data: "title_mutator",
orderable: true,
searchable: true,
searchable: false,
className: "size-px whitespace-nowrap",
render: (data: string, type: string) => {
if (type !== "display") return data;
@ -110,7 +108,7 @@ const columnDefs: ConfigColumnDefs[] = [
target: 4,
data: "description_mutator",
orderable: true,
searchable: true,
searchable: false,
className: "size-px whitespace-nowrap",
render: (data: string, type: string) => {
if (type !== "display") return data;
@ -285,10 +283,20 @@ const tableOptions: IDataTableOptions = {
}
};
const table: HSDataTable = new HSDataTable(
$("#table").get(0) as HTMLElement,
tableOptions
);
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();
@ -299,15 +307,11 @@ const onTableSelectChange = () => {
selectedRowsCount === 0 ? $elem.hide() : $elem.show();
};
(table as any).dataTable
.on("select", onTableSelectChange)
.on("deselect", onTableSelectChange)
.on("draw", onTableSelectChange);
$("#selectAllBox").on("change", function() {
const dt: Api = (table as any).dataTable;
(this as HTMLInputElement).checked
? (table as any).dataTable.rows().select()
: (table as any).dataTable.rows().deselect();
? dt.rows().select()
: dt.rows().deselect();
});
$("#deleteRowsBtn").on("click", async () => {
@ -332,23 +336,7 @@ $("#deleteRowsBtn").on("click", async () => {
// #endregion
// #region Page Size Select
// https://preline.co/plugins/html/advanced-select.html
(window as any).$hsSelectCollection = [];
(window as any)["FloatingUIDOM"] = {
computePosition: computePosition,
autoUpdate: autoUpdate,
offset: offset
};
// Close on click.
window.addEventListener('click', (evt) => {
const evtTarget = evt.target;
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
});
// #region Table Paging Select
const pageSelectOptions: ISelectOptions = {
toggleTag: '<button type="button" aria-expanded="false"></button>',
@ -368,29 +356,47 @@ const pageSelectOptions: ISelectOptions = {
dropdownVerticalFixedPlacement: null
};
const pageSizeSelect: HSSelect = new HSSelect(
$("#selectPageSize-js").get(0) as HTMLElement,
pageSelectOptions
);
window.addEventListener("preline:ready", () => {
const selectEl = $("#selectPageSize-js").get(0);
if (!HSSelect.getInstance(selectEl, true)) {
new HSSelect(selectEl, pageSelectOptions);
}
});
// #endregion
// #region Edit Modal
(window as any).$hsOverlayCollection = [];
const closeEditModal = () => { editModal.close() };
const editModalOptions: IOverlayOptions = {};
const openEditModal = async (id: number | undefined) => {
$("#editForm").removeClass("submitted");
editModal.open();
const editModal: HSOverlay = new HSOverlay(
$("#editModal").get(0) as HTMLElement,
editModalOptions
);
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");
@ -416,7 +422,7 @@ const loadEditModalData = async (id: number) => {
$(editModal.el).data("id", style.id);
$("#formName").val(style.name);
updateColourInput("#5865F2");
updateColourInput(style.colour);
titleMutatorSelect.setValue(style.title_mutator || "");
descriptionMutatorSelect.setValue(style.description_mutator || "");
@ -426,93 +432,19 @@ const loadEditModalData = async (id: number) => {
$("#formShowThumbnail").prop("checked", style.show_thumbnail);
$("#formShowFooter").prop("checked", style.show_footer);
$("#formShowTimestamp").prop("checked", style.show_timestamp);
}
const openEditModal = async (id: number | undefined) => {
$("#editForm").removeClass("submitted");
editModal.open();
id === undefined
? clearEditModalData()
: loadEditModalData(id);
};
const closeEditModal = () => {
editModal.close();
};
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
};
const titleMutatorSelect = new HSSelect(
$("#formTitleMutator").get(0),
mutatorSelectOptions
);
// Add options to title mutator select
titleMutatorSelect.addOption({ title: "None", val: "" });
Object.entries(textMutators).forEach(([key, description]) => {
titleMutatorSelect.addOption({
title: description,
val: key
} as ISingleOption)
});
const descriptionMutatorSelect = new HSSelect(
$("#formDescriptionMutator").get(0),
mutatorSelectOptions
);
// Add options to description mutator select
descriptionMutatorSelect.addOption({ title: "None", val: "" });
Object.entries(textMutators).forEach(([key, description]) => {
descriptionMutatorSelect.addOption({
title: description,
val: key
} as ISingleOption)
});
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)));
$("#editForm").on("submit", async event => {
event.preventDefault();
const form = $(event.target).get(0) as HTMLFormElement;
$(form).addClass("submitted");
if (!form.checkValidity()) return;
const validity = form.checkValidity();
if (!validity) {
console.debug(`Submit form invalid: ${validity}`);
return;
};
let method = "post";
const data = $(event.target).serializeArray();
@ -539,4 +471,66 @@ $("#editForm").on("submit", async event => {
});
});
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

View File

@ -1,593 +0,0 @@
import { formatTimestamp, verifyChannels } from "../main";
import HSDropdown from "preline/dist/dropdown";
import HSDataTable, { IDataTableOptions } from "preline/dist/datatable";
import HSSelect, { ISelectOptions } from "preline/dist/select";
import HSOverlay, { IOverlayOptions } from "preline/dist/overlay";
import HSDatepicker, { ICustomDatepickerOptions } from "preline/dist/datepicker";
import { AjaxSettings, ConfigColumnDefs } from "datatables.net-dt";
import prisma from "../../../../../generated/prisma";
declare let guildId: string;
declare let channels: Array<any>;
// #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 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></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 feed.
Alternatively, use a template to deploy a ready-made feed.
</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 feed
</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,
className: "size-px whitespace-nowrap",
render: (_data: unknown, _type: unknown, row: prisma.Feed) => { 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: "url",
orderable: true,
searchable: true,
className: "size-px whitespace-nowrap",
render: (data: string) => { return `
<a href="${data}" class="cj-table-link max-w-[450px] truncate">
${data}
</a>
`}
},
{
target: 3,
data: "channels",
className: "size-px",
render: (data: prisma.Channel[], type: string, row: prisma.Feed) => {
if (type !== "display") { return data; }
if (!data.length) { return ""; }
const wrapper = $("<div>").addClass("flex flex-nowrap gap-1 px-6 py-4");
const tag = $("<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");
if (!verifyChannels(data, channels)) {
wrapper.text("invalid channels").addClass("whitespace-nowrap");
return wrapper.get(0);
}
const firstChannelName = "# " + channels.find(c => c.id === data[0].channel_id).name;
wrapper.append(tag.clone().text(firstChannelName));
// No need to run the dropdown code if there's no more to show
if (data.length === 1) {
return wrapper.get(0);
}
data.shift();
if (data.length <= 1) {
const secondChannelName = "# " + channels.find(c => c.id === data[0].channel_id).name;
wrapper.append(tag.clone().text(secondChannelName));
return wrapper.get(0);
}
const dropdown = $("<div>").addClass("hs-dropdown inline-block");
const dropdownBtn = $("<button>").attr("id", `channelDrop-${row.id}`).attr("type", "button").addClass("cursor-pointer inline-flex items-center 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");
const dropdownMenu = $("<div>").addClass("hs-dropdown-menu hidden opacity-0 hs-dropdown-open:opacity-100 transition-[opacity,margin] overflow-hidden z-10 w-fit max-w-64 border p-2 rounded-md bg-gray-200 dark:bg-neutral-700 border-gray-300 dark:border-neutral-600");
dropdown.append(dropdownBtn.text(`+${data.length}`));
data.forEach(channel => {
const channelName = "# " + channels.find(c => c.id === channel.channel_id).name;
dropdownMenu.append(tag.clone().text(channelName));
});
dropdown.append(dropdownMenu);
wrapper.append(dropdown);
return wrapper.get(0);
}
},
{
target: 4,
data: "filters",
className: "size-px whitespace-nowrap",
render: (data: prisma.Filter[], type: string, row: prisma.Feed) => {
if (type !== "display") return data;
if (!data.length) return "";
const wrapper = $("<div>").addClass("flex flex-nowrap gap-1 px-6 py-4");
const tag = $("<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");
wrapper.append(tag.clone().text(data[0].name));
if (data.length === 1) {
return wrapper.get(0);
}
data.shift();
if (data.length <= 1) {
wrapper.append(tag.clone().text(data[0].name));
return wrapper.get(0);
}
const dropdown = $("<div>").addClass("hs-dropdown inline-block");
const dropdownBtn = $("<button>").attr("id", `channelDrop-${row.id}`).attr("type", "button").addClass("cursor-pointer inline-flex items-center 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");
const dropdownMenu = $("<div>").addClass("hs-dropdown-menu hidden opacity-0 hs-dropdown-open:opacity-100 transition-[opacity,margin] overflow-hidden z-10 w-fit max-w-64 border p-2 rounded-md bg-gray-200 dark:bg-neutral-700 border-gray-300 dark:border-neutral-600");
dropdown.append(dropdownBtn.text(`+${data.length}`));
data.forEach(filter => {
dropdownMenu.append(tag.clone().text(filter.name));
});
dropdown.append(dropdownMenu);
wrapper.append(dropdown);
return wrapper.get(0);
}
},
{
target: 5,
data: null, // "message_style_id"
className: "size-px whitespace-nowrap",
render: (_data: unknown, type: string, row: any) => {
if (!row.message_style || type !== "display") return null;
const wrapper = $("<div>").addClass("flex px-6 py-4");
const badge = $("<span>").addClass("inline-flex items-center whitespace-nowrap border rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 overflow-hidden");
const colour = $("<span>").addClass("size-6 shrink-0").css("background-color", row.message_style.colour);
const label = $("<span>").addClass("py-1 px-2.5 text-xs text-gray-800 dark:text-neutral-200");
label.text(row.message_style.name);
badge.append(colour).append(label);
wrapper.append(badge);
return wrapper.get(0);
}
},
{
target: 6,
data: "created_at",
orderable: true,
className: "size-px whitespace-nowrap",
render: (data: string) => { return `
<div class="px-6 py-4">
<span class="cj-table-text">
${formatTimestamp(data)}
</span>
</div>
`}
},
{
target: 7,
data: "active",
orderable: true,
className: "size-px whitespace-nowrap",
render: (data: boolean) => {
const wrapper = $("<div>").addClass("px-6 py-4");
const badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center gap-x-1 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($('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg>'))
.append(label.text("Active"));
} else {
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
badge.append($('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg>'))
.append(label.text("Inactive"));
}
wrapper.append(badge);
return wrapper.get(0);
}
}
];
const ajaxSettings: AjaxSettings = {
url: `/guild/${guildId}/feeds/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)) {
table = new HSDataTable(tableEl, tableOptions);
}
});
// #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
interface ExpandedFeed extends prisma.Feed {
channels: prisma.Channel[],
filters: prisma.Feed[]
}
const clearEditModalData = () => {
$(editModal.el).removeData("id");
$("#formName").val("");
$("#formUrl").val("");
$("#formActive").prop("checked", true);
channelSelect.setValue([]);
filterSelect.setValue([]);
styleSelect.setValue("");
};
const loadEditModalData = async (id: number) => {
const feed: ExpandedFeed = await $.ajax({
url: `/guild/${guildId}/feeds/api?id=${id}`,
method: "get"
});
$(editModal.el).data("id", feed.id);
$("#formName").val(feed.name);
$("#formUrl").val(feed.url);
$("#formActive").prop("checked", feed.active);
channelSelect.setValue(feed.channels.map(channel => channel.channel_id));
filterSelect.setValue(feed.filters.map(filter => `${filter.id}`));
styleSelect.setValue(`${feed.message_style_id}`);
}
$("#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}/feeds/api`,
dataType: "json",
method: method,
data: data,
success: () => {
(table as any).dataTable.draw()
closeEditModal();
},
error: error => {
alert(JSON.stringify(error, null, 4));
}
});
});
const channelSelectOptions: ISelectOptions = {
placeholder: "Select option....",
mode: "tags",
tagsItemTemplate: `
<div class="flex flex-nowrap items-center relative z-10 bg-white border border-gray-200 rounded-lg p-1 m-1 dark:bg-neutral-900 dark:border-neutral-700 ">
<div class="size-6 flex justify-center items-center" data-icon></div>
<div class="whitespace-nowrap text-gray-800 dark:text-neutral-200" data-title></div>
<div class="inline-flex shrink-0 justify-center items-center size-5 ms-2 rounded-lg text-gray-800 bg-gray-200 hover:bg-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 text-sm dark:bg-neutral-700/50 dark:hover:bg-neutral-700 dark:text-neutral-400 cursor-pointer" data-remove>
<svg class="shrink-0 size-3" 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</div>
</div>
`,
optionTemplate: `
<div class="cj-tag-select-option">
<div data-icon></div>
<div>
<div data-title></div>
<div data-description></div>
</div>
<div class="ms-auto">
<span class="hidden hs-selected:block">
<svg class="shrink-0 size-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"/></svg>
</span>
</div>
</div>
`,
tagsInputId: "formChannelsInput",
wrapperClasses: "cj-tag-select-wrapper",
dropdownClasses: "cj-tag-select-dropdown w-full",
tagsInputClasses: "cj-tag-select-input",
dropdownScope: "window",
dropdownSpace: 10,
dropdownPlacement: "bottom",
dropdownVerticalFixedPlacement: null,
hasSearch: false,
searchNoResultClasses: "cj-tag-select-search-no-results",
};
const filterSelectOptions: ISelectOptions = {
placeholder: "Select option....",
mode: "tags",
tagsItemTemplate: `
<div class="flex flex-nowrap items-center relative z-10 bg-white border border-gray-200 rounded-lg p-1 m-1 dark:bg-neutral-900 dark:border-neutral-700 ">
<div class="size-6 flex justify-center items-center">
<svg class="shrink-0 size-[16px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" /></svg>
</div>
<div class="whitespace-nowrap text-gray-800 dark:text-neutral-200" data-title></div>
<div class="inline-flex shrink-0 justify-center items-center size-5 ms-2 rounded-lg text-gray-800 bg-gray-200 hover:bg-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 text-sm dark:bg-neutral-700/50 dark:hover:bg-neutral-700 dark:text-neutral-400 cursor-pointer" data-remove>
<svg class="shrink-0 size-3" 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</div>
</div>
`,
optionTemplate: `
<div class="cj-tag-select-option">
<div data-icon>
<svg class="shrink-0 size-[18px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" /></svg>
</div>
<div>
<div data-title></div>
<div data-description></div>
</div>
<div class="ms-auto">
<span class="hidden hs-selected:block">
<svg class="shrink-0 size-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"/></svg>
</span>
</div>
</div>
`,
tagsInputId: "formFiltersInput",
wrapperClasses: "cj-tag-select-wrapper",
dropdownClasses: "cj-tag-select-dropdown w-full",
tagsInputClasses: "cj-tag-select-input",
dropdownScope: "window",
dropdownSpace: 10,
dropdownPlacement: "bottom",
dropdownVerticalFixedPlacement: null,
// API
apiUrl: `/guild/${guildId}/filters/api/select`,
apiQuery: "limit=15",
apiFieldsMap: {
id: "id",
val: "id",
title: "name",
description: "value",
name: "title"
},
apiSearchQueryKey: "search",
hasSearch: false,
searchNoResultClasses: "cj-tag-select-search-no-results",
};
const styleSelectOptions: ISelectOptions = {
placeholder: "Select option...",
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,
apiUrl: `/guild/${guildId}/styles/api/select`,
// apiQuery: "limit=15",
apiFieldsMap: {
id: "id",
val: "id",
title: "name",
description: "value",
name: "title"
},
apiSearchQueryKey: "search",
hasSearch: false,
optionAllowEmptyOption: true
};
let channelSelect: HSSelect;
let filterSelect: HSSelect;
let styleSelect: HSSelect;
window.addEventListener("preline:ready", () => {
const exists = (element: HTMLElement) => HSSelect.getInstance(element, true);
const channelEl = $("#formChannels").get(0);
const filterEl = $("#formFilters").get(0);
const styleEl = $("#formMessageStyle").get(0);
if (!exists(channelEl) && !exists(filterEl) && !exists(styleEl)) {
channelSelect = new HSSelect(channelEl, channelSelectOptions);
filterSelect = new HSSelect(filterEl, filterSelectOptions);
styleSelect = new HSSelect(styleEl, styleSelectOptions);
}
});
const publishedThresholdOptions: ICustomDatepickerOptions = {
type: "default",
dateMax: "2050-00-00",
mode: "custom-select",
layouts: {
default: `
<div class="--single-month flex flex-col overflow-hidden dark:bg-neutral-900 dark:border-neutral-700">
<div class="grid grid-cols-5 items-center gap-x-3 mx-1.5 pb-3" data-vc="header">
<div class="col-span-1">
<#CustomArrowPrev />
</div>
<div class="col-span-3 flex justify-center items-center gap-x-1">
<#CustomMonth />
<span class="text-gray-800 dark:text-neutral-200">/</span>
<#CustomYear />
</div>
<div class="col-span-1 flex justify-end">
<#CustomArrowNext />
</div>
</div>
<div data-vc="wrapper">
<div data-vc="content">
<#Week /><#Dates />
</div>
</div>
</div>
<#CustomTime />`
},
inputModeOptions: { itemsSeparator: " / " },
templates: {
arrowPrev: '<button data-vc-arrow="prev"><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"></path></svg></button>',
arrowNext: '<button data-vc-arrow="next"><svg class="shrink-0 size-4" 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"></path></svg></button>'
},
};
let publishedThresholdInput: HSDatepicker;
window.addEventListener("preline:ready", () => {
const publishedEl = $("#formPublishedThreshold").get(0);
if (!HSDatepicker.getInstance(publishedEl, true)) {
publishedThresholdInput = new HSDatepicker(publishedEl, publishedThresholdOptions);
}
});
// #endregion

View File

@ -78,7 +78,7 @@
</div>
</th>
<th scope="col" class="cj-table-header --exclude-from-ordering">
<div class="cj-table-header-content cursor-pointer">
<div class="cj-table-header-content">
<span>Style</span>
</div>
</th>
@ -176,17 +176,6 @@
<label for="formChannels" class="text-input-label">Channels</label>
<select name="channels" id="formChannels" class="--prevent-on-load-init" multiple>
<option value="">Choose</option>
<% guild.channels.cache
.filter(channel => channel.type == 0)
.sort((a, b) => a.rawPosition - b.rawPosition)
.forEach(channel => { %>
<option value="<%= channel.id %>" data-hs-select-option='{
"description": "<%= channel.id %>",
"icon": "<svg class=\"shrink-0 size-[16px]\" viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"4\" y1=\"9\" x2=\"20\" y2=\"9\"></line><line x1=\"4\" y1=\"15\" x2=\"20\" y2=\"15\"></line><line x1=\"10\" y1=\"3\" x2=\"8\" y2=\"21\"></line><line x1=\"16\" y1=\"3\" x2=\"14\" y2=\"21\"></line></svg>"
}'>
<%= channel.name %>
</option>
<% }); %>
</select>
<p class="text-input-help">
The recipients of content from this feed.
@ -207,14 +196,14 @@
<option value="">None</option>
</select>
<p class="text-input-help">
placeholder.
A custom appearance used to display content from this feed.
</p>
</div>
<div>
<label for="formPublishedThreshold" class="text-input-label">Published Threshold</label>
<input type="text" id="formPublishedThreshold" name="published_threshold" class="z-[80] py-3 px-4 block w-full border-gray-200 rounded-lg sm:text-sm focus:border-blue-600 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder:text-neutral-400 dark:focus:border-blue-500 dark:focus:ring-neutral-500" readonly required>
<input type="datetime-local" id="formPublishedThreshold" name="published_threshold" class="text-input form-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">
placeholder helper text.
This feed won't process content older than this date &amp; time.
</p>
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
Please enter a date.
@ -255,4 +244,4 @@
.map(channel => channel.toJSON())
) %>`);
</script>
<% block("scripts").append('<script src="/public/generated/js/guild/test.js"></script>'); %>
<% block("scripts").append('<script src="/public/generated/js/guild/feeds.js"></script>'); %>

View File

@ -142,7 +142,7 @@
</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">
<div id="editModal" class="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-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="mb-8">

View File

@ -174,7 +174,7 @@
</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">
<div id="editModal" class="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="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">

View File

@ -63,7 +63,7 @@ export const post = async (request: Request, response: Response) => {
channels: { create: formattedChannels },
filters: { connect: formattedFilters },
message_style_id: message_style === "" ? null : Number(message_style),
published_threshold: published_threshold
published_threshold: new Date(published_threshold)
}
});
}
@ -130,7 +130,7 @@ export const patch = async (request: Request, response: Response) => {
message_style === ""
? null
: Number(message_style),
published_threshold: published_threshold
published_threshold: new Date(published_threshold)
}
});
}

View File

@ -2,14 +2,5 @@ import { PrismaClient, Prisma } from "@server/../../generated/prisma";
const prisma = new PrismaClient();
// const resolvePrismaCodeToHttpCode = (error: Prisma.PrismaClientKnownRequestError) => {
// switch (error.code) {
// case "P2011":
// return 0;
// default:
// throw new Error(`Unhandled prisma error code: '${error.code}'`);
// }
// }
export { Prisma };
export default prisma;
export default prisma;