mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
feat: implement position management for page monitors and update related functionality
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.integer("position").unsigned().notNullable().defaultTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.dropColumn("position");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
page_id: pageId,
|
||||
monitor_tag: "earth",
|
||||
monitor_settings_json: "",
|
||||
position: 0,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
@@ -42,6 +43,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
page_id: pageId,
|
||||
monitor_tag: "kener",
|
||||
monitor_settings_json: "",
|
||||
position: 1,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ export async function AddMonitorToPage(
|
||||
page_id: number,
|
||||
monitor_tag: string,
|
||||
monitor_settings_json?: string | null,
|
||||
position?: number,
|
||||
): Promise<void> {
|
||||
// Check if page exists
|
||||
const page = await db.getPageById(page_id);
|
||||
@@ -120,13 +121,38 @@ export async function AddMonitorToPage(
|
||||
throw new Error(`Monitor "${monitor_tag}" already exists on this page`);
|
||||
}
|
||||
|
||||
// If no position specified, append at end
|
||||
let finalPosition = position;
|
||||
if (finalPosition === undefined) {
|
||||
const existing = await db.getPageMonitors(page_id);
|
||||
finalPosition = existing.length > 0 ? Math.max(...existing.map((m) => m.position)) + 1 : 0;
|
||||
}
|
||||
|
||||
await db.addMonitorToPage({
|
||||
page_id,
|
||||
monitor_tag,
|
||||
monitor_settings_json: monitor_settings_json || null,
|
||||
position: finalPosition,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder monitors on a page
|
||||
*/
|
||||
export async function ReorderPageMonitors(page_id: number, monitor_tags: string[]): Promise<void> {
|
||||
const page = await db.getPageById(page_id);
|
||||
if (!page) {
|
||||
throw new Error(`Page with id ${page_id} not found`);
|
||||
}
|
||||
|
||||
const monitorPositions = monitor_tags.map((tag, index) => ({
|
||||
monitor_tag: tag,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
await db.updatePageMonitorPositions(page_id, monitorPositions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove monitor from a page
|
||||
*/
|
||||
|
||||
@@ -212,6 +212,7 @@ class DbImpl {
|
||||
monitorExistsOnPage!: PagesRepository["monitorExistsOnPage"];
|
||||
deletePageMonitorsByTag!: PagesRepository["deletePageMonitorsByTag"];
|
||||
deletePageMonitorsByPageId!: PagesRepository["deletePageMonitorsByPageId"];
|
||||
updatePageMonitorPositions!: PagesRepository["updatePageMonitorPositions"];
|
||||
|
||||
// ============ Maintenances ============
|
||||
createMaintenance!: MaintenancesRepository["createMaintenance"];
|
||||
@@ -554,6 +555,7 @@ class DbImpl {
|
||||
this.monitorExistsOnPage = this.pages.monitorExistsOnPage.bind(this.pages);
|
||||
this.deletePageMonitorsByTag = this.pages.deletePageMonitorsByTag.bind(this.pages);
|
||||
this.deletePageMonitorsByPageId = this.pages.deletePageMonitorsByPageId.bind(this.pages);
|
||||
this.updatePageMonitorPositions = this.pages.updatePageMonitorPositions.bind(this.pages);
|
||||
}
|
||||
|
||||
private bindMaintenancesMethods(): void {
|
||||
|
||||
@@ -65,6 +65,7 @@ export class PagesRepository extends BaseRepository {
|
||||
page_id: data.page_id,
|
||||
monitor_tag: data.monitor_tag,
|
||||
monitor_settings_json: data.monitor_settings_json,
|
||||
position: data.position ?? 0,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
@@ -75,7 +76,7 @@ export class PagesRepository extends BaseRepository {
|
||||
}
|
||||
|
||||
async getPageMonitors(page_id: number): Promise<PageMonitorRecord[]> {
|
||||
return await this.knex("pages_monitors").where("page_id", page_id).orderBy("created_at", "desc");
|
||||
return await this.knex("pages_monitors").where("page_id", page_id).orderBy("position", "asc");
|
||||
}
|
||||
|
||||
async getPageMonitorsExcludeHidden(page_id: number): Promise<PageMonitorRecord[]> {
|
||||
@@ -84,7 +85,7 @@ export class PagesRepository extends BaseRepository {
|
||||
.where("pages_monitors.page_id", page_id)
|
||||
.andWhere("monitors.is_hidden", "NO")
|
||||
.andWhere("monitors.status", "ACTIVE")
|
||||
.orderBy("pages_monitors.created_at", "desc")
|
||||
.orderBy("pages_monitors.position", "asc")
|
||||
.select("pages_monitors.*");
|
||||
}
|
||||
|
||||
@@ -115,4 +116,17 @@ export class PagesRepository extends BaseRepository {
|
||||
async deletePageMonitorsByPageId(page_id: number): Promise<number> {
|
||||
return await this.knex("pages_monitors").where({ page_id }).del();
|
||||
}
|
||||
|
||||
async updatePageMonitorPositions(
|
||||
page_id: number,
|
||||
monitorPositions: { monitor_tag: string; position: number }[],
|
||||
): Promise<void> {
|
||||
await this.knex.transaction(async (trx) => {
|
||||
for (const mp of monitorPositions) {
|
||||
await trx("pages_monitors")
|
||||
.where({ page_id, monitor_tag: mp.monitor_tag })
|
||||
.update({ position: mp.position, updated_at: trx.fn.now() });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +463,7 @@ export interface PageMonitorRecord {
|
||||
page_id: number;
|
||||
monitor_tag: string;
|
||||
monitor_settings_json: string | null;
|
||||
position: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -471,12 +472,14 @@ export interface PageMonitorRecordInsert {
|
||||
page_id: number;
|
||||
monitor_tag: string;
|
||||
monitor_settings_json?: string | null;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
export interface PageMonitorRecordTyped {
|
||||
page_id: number;
|
||||
monitor_tag: string;
|
||||
monitor_settings: Record<string, unknown> | null;
|
||||
position: number;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ async function formatPageResponse(page: PageRecord): Promise<PageResponse> {
|
||||
page_subheader: page.page_subheader,
|
||||
page_logo: page.page_logo,
|
||||
page_settings: pageSettings,
|
||||
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag })),
|
||||
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag, position: pm.position })),
|
||||
created_at: formatDateToISO(page.created_at),
|
||||
updated_at: formatDateToISO(page.updated_at),
|
||||
};
|
||||
@@ -221,11 +221,12 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
|
||||
// Add monitors to the page
|
||||
if (body.monitors && Array.isArray(body.monitors)) {
|
||||
for (const monitorTag of body.monitors) {
|
||||
for (let i = 0; i < body.monitors.length; i++) {
|
||||
await db.addMonitorToPage({
|
||||
page_id: createdPage.id,
|
||||
monitor_tag: monitorTag,
|
||||
monitor_tag: body.monitors[i],
|
||||
monitor_settings_json: null,
|
||||
position: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ async function formatPageResponse(page: PageRecord): Promise<PageResponse> {
|
||||
page_subheader: page.page_subheader,
|
||||
page_logo: page.page_logo,
|
||||
page_settings: pageSettings,
|
||||
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag })),
|
||||
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag, position: pm.position })),
|
||||
created_at: formatDateToISO(page.created_at),
|
||||
updated_at: formatDateToISO(page.updated_at),
|
||||
};
|
||||
@@ -291,11 +291,12 @@ export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
await db.deletePageMonitorsByPageId(page.id);
|
||||
|
||||
// Add new monitors
|
||||
for (const monitorTag of body.monitors) {
|
||||
for (let i = 0; i < body.monitors.length; i++) {
|
||||
await db.addMonitorToPage({
|
||||
page_id: page.id,
|
||||
monitor_tag: monitorTag,
|
||||
monitor_tag: body.monitors[i],
|
||||
monitor_settings_json: null,
|
||||
position: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
AddMonitorToPage,
|
||||
RemoveMonitorFromPage,
|
||||
GetPageMonitors,
|
||||
ReorderPageMonitors,
|
||||
} from "$lib/server/controllers/pagesController.js";
|
||||
import {
|
||||
CreateMaintenance,
|
||||
@@ -409,6 +410,10 @@ export async function POST({ request, cookies }) {
|
||||
AdminEditorCan(userDB.role);
|
||||
await RemoveMonitorFromPage(data.page_id, data.monitor_tag);
|
||||
resp = { success: true };
|
||||
} else if (action == "reorderPageMonitors") {
|
||||
AdminEditorCan(userDB.role);
|
||||
await ReorderPageMonitors(data.page_id, data.monitor_tags);
|
||||
resp = { success: true };
|
||||
}
|
||||
// ============ Maintenance Actions ============
|
||||
else if (action == "getMaintenances") {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import SaveIcon from "@lucide/svelte/icons/save";
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
|
||||
import ArrowDownIcon from "@lucide/svelte/icons/arrow-down";
|
||||
import UploadIcon from "@lucide/svelte/icons/upload";
|
||||
import ImageIcon from "@lucide/svelte/icons/image";
|
||||
import TrashIcon from "@lucide/svelte/icons/trash";
|
||||
@@ -68,6 +70,7 @@
|
||||
let selectedMonitors = $state<string[]>([]);
|
||||
let addingMonitor = $state(false);
|
||||
let removingMonitor = $state<string | null>(null);
|
||||
let reordering = $state(false);
|
||||
|
||||
// Delete state
|
||||
let deleteConfirmText = $state("");
|
||||
@@ -231,7 +234,7 @@
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success("Monitor added to page");
|
||||
selectedMonitors = [selectedMonitorTag, ...selectedMonitors.filter((tag) => tag !== selectedMonitorTag)];
|
||||
selectedMonitors = [...selectedMonitors, selectedMonitorTag];
|
||||
selectedMonitorTag = "";
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -303,6 +306,39 @@
|
||||
// Get available monitors (not already on the current page)
|
||||
const availableMonitors = $derived(monitors.filter((m) => !selectedMonitors.includes(m.tag)));
|
||||
|
||||
async function moveMonitor(index: number, direction: "up" | "down") {
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= selectedMonitors.length) return;
|
||||
|
||||
const updated = [...selectedMonitors];
|
||||
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
|
||||
selectedMonitors = updated;
|
||||
|
||||
if (!currentPage) return;
|
||||
reordering = true;
|
||||
try {
|
||||
const response = await fetch(clientResolver(resolve, "/manage/api"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "reorderPageMonitors",
|
||||
data: {
|
||||
page_id: currentPage.id,
|
||||
monitor_tags: selectedMonitors
|
||||
}
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("Failed to reorder monitors");
|
||||
} finally {
|
||||
reordering = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Image upload functions
|
||||
async function handleLogoUpload(event: Event): Promise<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
@@ -614,25 +650,43 @@
|
||||
<Label>Current Monitors</Label>
|
||||
{#if selectedMonitors.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each selectedMonitors as monitorTag (monitorTag)}
|
||||
{#each selectedMonitors as monitorTag, i (monitorTag)}
|
||||
{@const monitor = monitors.find((m) => m.tag === monitorTag)}
|
||||
<div class="bg-muted flex items-center justify-between rounded-lg p-3">
|
||||
<div>
|
||||
<p class="font-medium">{monitor?.name || monitorTag}</p>
|
||||
<p class="text-muted-foreground text-xs">{monitorTag}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => removeMonitorFromPage(monitorTag)}
|
||||
disabled={removingMonitor === monitorTag}
|
||||
>
|
||||
{#if removingMonitor === monitorTag}
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<XIcon class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => moveMonitor(i, "up")}
|
||||
disabled={i === 0 || reordering}
|
||||
>
|
||||
<ArrowUpIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => moveMonitor(i, "down")}
|
||||
disabled={i === selectedMonitors.length - 1 || reordering}
|
||||
>
|
||||
<ArrowDownIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => removeMonitorFromPage(monitorTag)}
|
||||
disabled={removingMonitor === monitorTag}
|
||||
>
|
||||
{#if removingMonitor === monitorTag}
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<XIcon class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user