diff --git a/CHANGELOG.md b/CHANGELOG.md index 039759f..c30126f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.json b/package.json index 7ca1fd8..87fe8c6 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/client/src/ts/guild/feeds.ts b/src/client/src/ts/guild/feeds.ts index 4fd66fb..6ceb339 100644 --- a/src/client/src/ts/guild/feeds.ts +++ b/src/client/src/ts/guild/feeds.ts @@ -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; - // #region DataTable -// const emptyTableHtml: string = `
@@ -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: '', @@ -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: '', - arrowNext: '' - } -}; + 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: ` + + + + + + ` // hashtag icon + } + }); + }) }); // #endregion diff --git a/src/client/src/ts/guild/filters.ts b/src/client/src/ts/guild/filters.ts index f95449a..15816f0 100644 --- a/src/client/src/ts/guild/filters.ts +++ b/src/client/src/ts/guild/filters.ts @@ -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 = `
@@ -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: '', @@ -270,35 +257,52 @@ const pageSelectOptions: ISelectOptions = {
`, 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: '', @@ -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 + }); }); }); diff --git a/src/client/src/ts/guild/styles.ts b/src/client/src/ts/guild/styles.ts index ec94924..92139cd 100644 --- a/src/client/src/ts/guild/styles.ts +++ b/src/client/src/ts/guild/styles.ts @@ -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 = `
@@ -44,7 +37,6 @@ const emptyTableHtml: string = `
`; - 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 ` -
- - ${data} - -
- `} + render: (data: string, type: string) => { + if (type !== "display") return data; + + const wrapper = $("
").addClass("flex px-6 py-4"); + const badge = $("").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 = $("").addClass("size-6 shrink-0").css("background-color", data); + const label = $("").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: '', @@ -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: '', - optionTemplate: ` -
- - -
`, - toggleClasses: "cj-select-toggle select-input", - optionClasses: "cj-select-option", - dropdownClasses: "cj-select-dropdown", - wrapperClasses: "peer", - dropdownSpace: 10, - dropdownScope: "parent", - dropdownPlacement: "top", - dropdownVerticalFixedPlacement: null -}; - -const 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; -const colourTextInput = $("#formColourInput") as JQuery; -const colourRandomBtn = $("#formColourRandomBtn") as JQuery; - -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: '', + optionTemplate: ` +
+ + +
`, + toggleClasses: "cj-select-toggle select-input", + optionClasses: "cj-select-option", + dropdownClasses: "cj-select-dropdown", + wrapperClasses: "peer", + dropdownSpace: 10, + dropdownScope: "parent", + dropdownPlacement: "top", + dropdownVerticalFixedPlacement: null +}; + +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; +const colourTextInput = $("#formColourInput") as JQuery; +const colourRandomBtn = $("#formColourRandomBtn") as JQuery; + +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 \ No newline at end of file diff --git a/src/client/src/ts/guild/test.ts b/src/client/src/ts/guild/test.ts deleted file mode 100644 index 5e734eb..0000000 --- a/src/client/src/ts/guild/test.ts +++ /dev/null @@ -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; - -// #region DataTable - -const emptyTableHtml: string = ` -
-
- -
-

- No results found -

-

- Refine your search or create a new feed. - Alternatively, use a template to deploy a ready-made feed. -

- -
- - -
-
-`; - -const columnDefs: ConfigColumnDefs[] = [ - // Select checkbox column - { - target: 0, - className: "size-px whitespace-nowrap", - render: (_data: unknown, _type: unknown, row: prisma.Feed) => { return ` -
- -
- `} - }, - { - target: 1, - data: "name", - orderable: true, - searchable: true, - className: "size-px whitespace-nowrap", - render: (data: string, _type: string, row: prisma.Feed) => { return ` - - ${data} - - `} - }, - { - target: 2, - data: "url", - orderable: true, - searchable: true, - className: "size-px whitespace-nowrap", - render: (data: string) => { return ` - - ${data} - - `} - }, - { - 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 = $("
").addClass("flex flex-nowrap gap-1 px-6 py-4"); - const tag = $("").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 = $("
").addClass("hs-dropdown inline-block"); - const dropdownBtn = $("', - optionTemplate: ` -
- - -
`, - 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: ` -
-
-
-
- -
-
- `, - optionTemplate: ` -
-
-
-
-
-
-
- -
-
- `, - 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: ` -
-
- -
-
-
- -
-
- `, - optionTemplate: ` -
-
- -
-
-
-
-
-
- -
-
- `, - 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: '', - optionTemplate: ` -
- - -
`, - toggleClasses: "cj-select-toggle select-input", - optionClasses: "cj-select-option", - dropdownClasses: "cj-select-dropdown", - wrapperClasses: "peer", - dropdownSpace: 10, - dropdownScope: "parent", - dropdownPlacement: "top", - dropdownVerticalFixedPlacement: null, - - 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: ` -
-
-
- <#CustomArrowPrev /> -
-
- <#CustomMonth /> - / - <#CustomYear /> -
-
- <#CustomArrowNext /> -
-
-
-
- <#Week /><#Dates /> -
-
-
- <#CustomTime />` - }, - inputModeOptions: { itemsSeparator: " / " }, - templates: { - arrowPrev: '', - arrowNext: '' - }, - -}; - -let publishedThresholdInput: HSDatepicker; - -window.addEventListener("preline:ready", () => { - const publishedEl = $("#formPublishedThreshold").get(0); - if (!HSDatepicker.getInstance(publishedEl, true)) { - publishedThresholdInput = new HSDatepicker(publishedEl, publishedThresholdOptions); - } -}); - -// #endregion diff --git a/src/client/views/guild/feeds.ejs b/src/client/views/guild/feeds.ejs index b7ee820..1e0e8d9 100644 --- a/src/client/views/guild/feeds.ejs +++ b/src/client/views/guild/feeds.ejs @@ -78,7 +78,7 @@
-
+
Style
@@ -176,17 +176,6 @@

The recipients of content from this feed. @@ -207,14 +196,14 @@

- placeholder. + A custom appearance used to display content from this feed.

- +

- placeholder helper text. + This feed won't process content older than this date & time.

-