feat: filter selection and table render for feed view
All checks were successful
Build / build (push) Successful in 48s

This commit is contained in:
Corban-Lee Jones 2025-05-05 14:41:53 +01:00
parent ff992fefa7
commit e935d801e6
5 changed files with 183 additions and 25 deletions

View File

@ -136,7 +136,7 @@
@apply
z-80
min-w-fit
min-h-[150px]
min-h-fit
max-h-72
p-1.5
space-y-0.5
@ -184,6 +184,38 @@
@apply text-xs text-gray-500 dark:text-neutral-500;
}
.cj-tag-select-search {
@apply
block
w-full
rounded-lg
py-1.5
sm:py-2
px-3
sm:text-sm
bg-white
text-gray-800
border-gray-200
dark:text-neutral-400
dark:bg-neutral-900
dark:border-neutral-700;
}
.cj-tag-select-search-wrapper {
@apply
p-2
-mx-1
-mt-1
sticky
top-0
bg-none
}
.cj-tag-select-search-no-results {
@apply block p-4;
}
/* Normal Select */
.cj-select-toggle {

View File

@ -109,16 +109,15 @@ const columnDefs: ConfigColumnDefs[] = [
if (data.length === 1) {
return wrapper.get(0);
}
else if (data.length <= 2) {
const secondChannelName = "# " + channels.find(c => c.id === data[1].channel_id).name;
data.shift();
if (data.length <= 1) {
const secondChannelName = "# " + channels.find(c => c.id === data[0].channel_id).name;
wrapper.append(tag.clone().text(secondChannelName));
data.shift();
return wrapper.get(0);
}
// drop the first element to exclude it from the dropdown
data.shift();
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");
@ -141,13 +140,40 @@ const columnDefs: ConfigColumnDefs[] = [
orderable: false,
searchable: false,
className: "size-px whitespace-nowrap",
render: (data: prisma.Filter[]) => { return `
<div class="px-6 py-4">
<span class="cj-table-text">
${data}
</span>
</div>
`}
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,
@ -406,13 +432,14 @@ const channelsSelectOptions: ISelectOptions = {
<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 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",
@ -422,14 +449,79 @@ const channelsSelectOptions: ISelectOptions = {
dropdownScope: "window",
dropdownSpace: 10,
dropdownPlacement: "bottom",
dropdownVerticalFixedPlacement: null
dropdownVerticalFixedPlacement: null,
hasSearch: false,
searchNoResultClasses: "cj-tag-select-search-no-results",
};
const channelSelect: HSSelect = new HSSelect(
const channelSelect = new HSSelect(
$("#formChannels").get(0) as HTMLElement,
channelsSelectOptions
);
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 filterSelect = new HSSelect(
$("#formFilters").get(0),
filterSelectOptions
);
$("#editForm").on("submit", async event => {
event.preventDefault();

View File

@ -197,9 +197,18 @@
</option>
<% }); %>
</select>
<p class="text-input-help">
The recipients of content from this feed.
</p>
</div>
<div>
<div class="relative">
<label for="formFilters" class="text-input-label">Filters</label>
<select name="filters" id="formFilters" class="--prevent-on-load-init" multiple>
<option value="">Choose</option>
</select>
<p class="text-input-help">
Filter out unwanted content from this feed.
</p>
</div>
<div></div>
<div></div>

View File

@ -2,6 +2,7 @@ import { Request, Response } from "express";
import prisma, { Prisma } from "@server/prisma";
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
// TODO: this doesn't account for guild ID or permissions
export const get = async (request: Request, response: Response) => {
if (!request.query.id) {
response.status(400).json({ error: "missing 'id' query" });
@ -118,4 +119,27 @@ export const datatable = async (request: Request, response: Response) => {
);
};
export default { get, post, patch, del, datatable };
export const select = async (request: Request, response: Response) => {
const guildId = request.params.guildId;
const { search } = request.query;
const data = await prisma.filter.findMany({
where: {
guild_id: guildId,
name: { contains: `${search}` }
}
});
// Preline Bug: https://github.com/htmlstreamofficial/preline/issues/567
// The returned data must have a "title" key, otherwise the advanced
// select component with 'tags' mode will have no title, regardless of
// mapping.
const modifiedResults = data.map(filter => ({
...filter,
title: filter.name
}));
response.json(modifiedResults);
};
export default { get, post, patch, del, datatable, select };

View File

@ -32,6 +32,7 @@ router.patch("/:guildId/feeds/api", feedApiController.patch);
router.delete("/:guildId/feeds/api", feedApiController.del);
router.post("/:guildId/filters/api/datatable", filterApiController.datatable);
router.get("/:guildId/filters/api/select", filterApiController.select);
router.get("/:guildId/filters/api", filterApiController.get);
router.post("/:guildId/filters/api", filterApiController.post);
router.patch("/:guildId/filters/api", filterApiController.patch);