feat: filter selection and table render for feed view
All checks were successful
Build / build (push) Successful in 48s
All checks were successful
Build / build (push) Successful in 48s
This commit is contained in:
parent
ff992fefa7
commit
e935d801e6
@ -136,7 +136,7 @@
|
|||||||
@apply
|
@apply
|
||||||
z-80
|
z-80
|
||||||
min-w-fit
|
min-w-fit
|
||||||
min-h-[150px]
|
min-h-fit
|
||||||
max-h-72
|
max-h-72
|
||||||
p-1.5
|
p-1.5
|
||||||
space-y-0.5
|
space-y-0.5
|
||||||
@ -184,6 +184,38 @@
|
|||||||
@apply text-xs text-gray-500 dark:text-neutral-500;
|
@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 */
|
/* Normal Select */
|
||||||
|
|
||||||
.cj-select-toggle {
|
.cj-select-toggle {
|
||||||
|
@ -109,16 +109,15 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
if (data.length === 1) {
|
if (data.length === 1) {
|
||||||
return wrapper.get(0);
|
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));
|
wrapper.append(tag.clone().text(secondChannelName));
|
||||||
data.shift();
|
|
||||||
return wrapper.get(0);
|
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 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 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");
|
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,
|
orderable: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
className: "size-px whitespace-nowrap",
|
className: "size-px whitespace-nowrap",
|
||||||
render: (data: prisma.Filter[]) => { return `
|
render: (data: prisma.Filter[], type: string, row: prisma.Feed) => {
|
||||||
<div class="px-6 py-4">
|
if (type !== "display") return data;
|
||||||
<span class="cj-table-text">
|
if (!data.length) return "";
|
||||||
${data}
|
|
||||||
</span>
|
const wrapper = $("<div>").addClass("flex flex-nowrap gap-1 px-6 py-4");
|
||||||
</div>
|
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,
|
target: 5,
|
||||||
@ -406,13 +432,14 @@ const channelsSelectOptions: ISelectOptions = {
|
|||||||
<div data-icon></div>
|
<div data-icon></div>
|
||||||
<div>
|
<div>
|
||||||
<div data-title></div>
|
<div data-title></div>
|
||||||
<div data-description></div></div>
|
<div data-description></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>
|
</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",
|
tagsInputId: "formChannelsInput",
|
||||||
wrapperClasses: "cj-tag-select-wrapper",
|
wrapperClasses: "cj-tag-select-wrapper",
|
||||||
@ -422,14 +449,79 @@ const channelsSelectOptions: ISelectOptions = {
|
|||||||
dropdownScope: "window",
|
dropdownScope: "window",
|
||||||
dropdownSpace: 10,
|
dropdownSpace: 10,
|
||||||
dropdownPlacement: "bottom",
|
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,
|
$("#formChannels").get(0) as HTMLElement,
|
||||||
channelsSelectOptions
|
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 => {
|
$("#editForm").on("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -197,9 +197,18 @@
|
|||||||
</option>
|
</option>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</select>
|
</select>
|
||||||
|
<p class="text-input-help">
|
||||||
|
The recipients of content from this feed.
|
||||||
|
</p>
|
||||||
</div>
|
</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></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
@ -2,6 +2,7 @@ import { Request, Response } from "express";
|
|||||||
import prisma, { Prisma } from "@server/prisma";
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
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) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
if (!request.query.id) {
|
if (!request.query.id) {
|
||||||
response.status(400).json({ error: "missing 'id' query" });
|
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 };
|
@ -32,6 +32,7 @@ router.patch("/:guildId/feeds/api", feedApiController.patch);
|
|||||||
router.delete("/:guildId/feeds/api", feedApiController.del);
|
router.delete("/:guildId/feeds/api", feedApiController.del);
|
||||||
|
|
||||||
router.post("/:guildId/filters/api/datatable", filterApiController.datatable);
|
router.post("/:guildId/filters/api/datatable", filterApiController.datatable);
|
||||||
|
router.get("/:guildId/filters/api/select", filterApiController.select);
|
||||||
router.get("/:guildId/filters/api", filterApiController.get);
|
router.get("/:guildId/filters/api", filterApiController.get);
|
||||||
router.post("/:guildId/filters/api", filterApiController.post);
|
router.post("/:guildId/filters/api", filterApiController.post);
|
||||||
router.patch("/:guildId/filters/api", filterApiController.patch);
|
router.patch("/:guildId/filters/api", filterApiController.patch);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user