diff --git a/src/client/src/ts/guild/feeds.ts b/src/client/src/ts/guild/feeds.ts index 4fd66fb..b3efbac 100644 --- a/src/client/src/ts/guild/feeds.ts +++ b/src/client/src/ts/guild/feeds.ts @@ -1,45 +1,18 @@ -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 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"; -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."); -// // } -// // }); +import { ISingleOption } from "preline"; +import { TextChannel } from "discord.js"; declare let guildId: string; declare let channels: Array; - // #region DataTable -// const emptyTableHtml: string = `
@@ -70,8 +43,6 @@ const columnDefs: ConfigColumnDefs[] = [ // Select checkbox column { target: 0, - orderable: false, - searchable: false, className: "size-px whitespace-nowrap", render: (_data: unknown, _type: unknown, row: prisma.Feed) => { return `
@@ -109,8 +80,6 @@ const columnDefs: ConfigColumnDefs[] = [ { target: 3, data: "channels", - orderable: false, - searchable: false, className: "size-px", render: (data: prisma.Channel[], type: string, row: prisma.Feed) => { if (type !== "display") { return data; } @@ -159,8 +128,6 @@ const columnDefs: ConfigColumnDefs[] = [ { target: 4, data: "filters", - orderable: false, - searchable: false, className: "size-px whitespace-nowrap", render: (data: prisma.Filter[], type: string, row: prisma.Feed) => { if (type !== "display") return data; @@ -200,8 +167,6 @@ const columnDefs: ConfigColumnDefs[] = [ { target: 5, data: null, // "message_style_id" - orderable: false, - searchable: false, className: "size-px whitespace-nowrap", render: (_data: unknown, type: string, row: any) => { if (!row.message_style || type !== "display") return null; @@ -221,7 +186,6 @@ const columnDefs: ConfigColumnDefs[] = [ target: 6, data: "created_at", orderable: true, - searchable: false, className: "size-px whitespace-nowrap", render: (data: string) => { return `
@@ -235,7 +199,6 @@ const columnDefs: ConfigColumnDefs[] = [ target: 7, data: "active", orderable: true, - searchable: false, className: "size-px whitespace-nowrap", render: (data: boolean) => { const wrapper = $("
").addClass("px-6 py-4"); @@ -292,68 +255,18 @@ const tableOptions: IDataTableOptions = { } }; -const table: HSDataTable = new HSDataTable( - $("#table").get(0) as HTMLElement, - tableOptions -); +let table: HSDataTable; -const onTableSelectChange = () => { - const selectedRowsCount = (table as any).dataTable.rows({ selected: true }).count(); - $("#deleteRowsBtn").prop("disabled", selectedRowsCount === 0); - $(".rows-selected-count-js").text(selectedRowsCount); - - const $elem = $(".rows-selected-count-js.zero-empty-js"); - selectedRowsCount === 0 ? $elem.hide() : $elem.show(); -}; - -(table as any).dataTable - .on("select", onTableSelectChange) - .on("deselect", onTableSelectChange) - .on("draw", onTableSelectChange); - -$("#selectAllBox").on("change", function() { - (this as HTMLInputElement).checked - ? (table as any).dataTable.rows().select() - : (table as any).dataTable.rows().deselect(); -}); - -$("#deleteRowsBtn").on("click", async () => { - const dt: Api = (table as any).dataTable; - const rowsData = dt.rows({ selected: true }).data().toArray(); - const rowIds = rowsData.map((row: prisma.Feed) => row.id); - - await $.ajax({ - url: `/guild/${guildId}/feeds/api`, - method: "delete", - dataType: "json", - data: { ids: rowIds }, - success: () => { - dt.draw(); - dt.rows().deselect(); - }, - error: error => { - alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error); - } - }); +window.addEventListener("preline:ready", () => { + const tableEl = $("#table").get(0); + if (!HSDataTable.getInstance(tableEl, true)) { + table = new HSDataTable(tableEl, tableOptions); + } }); // #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 +286,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 +337,7 @@ const clearEditModalData = () => { $("#formName").val(""); $("#formUrl").val(""); + $("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16)); $("#formActive").prop("checked", true); channelSelect.setValue([]); filterSelect.setValue([]); @@ -419,8 +352,11 @@ 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)); @@ -428,25 +364,44 @@ const loadEditModalData = async (id: number) => { 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(); +$("#editForm").on("submit", async event => { + event.preventDefault(); - id === undefined - ? clearEditModalData() - : loadEditModalData(id); -}; + const form = $(event.target).get(0) as HTMLFormElement; + $(form).addClass("submitted"); -const closeEditModal = () => { - editModal.close(); -}; + const validity = form.checkValidity(); + if (!validity) { + console.debug(`Submit form invalid: ${validity}`); + return; + }; -const channelsSelectOptions: ISelectOptions = { + 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 +442,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 +499,6 @@ const filterSelectOptions: ISelectOptions = { }; -const filterSelect = new HSSelect( - $("#formFilters").get(0), - filterSelectOptions -); - const styleSelectOptions: ISelectOptions = { placeholder: "Select option...", @@ -588,56 +533,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/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..c075276 100644 --- a/src/client/views/guild/feeds.ejs +++ b/src/client/views/guild/feeds.ejs @@ -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.