fix: address remaining widget review feedback

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:08 +02:00
1 parent 5553c4e0c5
commit f00f5b83b5
13 files changed
+149 -26

No files matched your search

+2 -2
View File
@@ -45,14 +45,14 @@ export const calendarRouter = createTRPCRouter({
};
},
{
fallback: (integration, error) => ({
fallback: (integration) => ({
events: [],
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
},
error: error instanceof Error ? error.message : String(error),
error: "Integration request failed",
}),
throwOnAllFailures: true,
},
@@ -44,10 +44,10 @@ export const mediaRequestsRouter = createTRPCRouter({
};
},
{
fallback: (integration, error) => ({
fallback: (integration) => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind },
data: [],
error: error instanceof Error ? error.message : String(error),
error: "Integration request failed",
}),
throwOnAllFailures: true,
},
@@ -101,10 +101,10 @@ export const mediaRequestsRouter = createTRPCRouter({
};
},
{
fallback: (integration, error) => ({
fallback: (integration) => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind },
data: { stats: null, users: [] },
error: error instanceof Error ? error.message : String(error),
error: "Integration request failed",
}),
throwOnAllFailures: true,
},
@@ -38,12 +38,12 @@ export const mediaServerRouter = createTRPCRouter({
};
},
{
fallback: (integration, error) => ({
fallback: (integration) => ({
integrationId: integration.id,
integrationName: integration.name,
integrationKind: integration.kind,
sessions: [],
error: error instanceof Error ? error.message : String(error),
error: "Integration request failed",
}),
throwOnAllFailures: true,
},
+7 -3
View File
@@ -9,6 +9,7 @@ import { boards, items } from "@homarr/db/schema";
import type { WidgetOptionsSettings } from "../../../../widgets/src";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
import { validateTimetableOptionsChangeAsync } from "./timetable";
import { throwIfCustomWidgetPlacementChangeForbidden } from "../board/custom-widget-placement-access";
export const optionsRouter = createTRPCRouter({
@@ -46,12 +47,15 @@ export const optionsRouter = createTRPCRouter({
});
}
const options = SuperJSON.parse<Record<string, unknown>>(item.options);
const updatedOptions = { ...options, ...input.newOptions };
const previousOptions = SuperJSON.parse<Record<string, unknown>>(item.options);
const updatedOptions = { ...previousOptions, ...input.newOptions };
if (item.kind === "timetable") {
await validateTimetableOptionsChangeAsync(updatedOptions, previousOptions);
}
throwIfCustomWidgetPlacementChangeForbidden({
isAdmin: ctx.session.user.permissions.includes("admin"),
submittedItems: [{ id: item.id, kind: item.kind, options: updatedOptions }],
storedItems: [{ id: item.id, kind: item.kind, options }],
storedItems: [{ id: item.id, kind: item.kind, options: previousOptions }],
});
await ctx.db
.update(items)
+29 -2
View File
@@ -156,6 +156,34 @@ const resolvePublicTimetableAddressesAsync = async (baseUrl: string): Promise<Ti
return addresses;
};
const normalizeTimetableOptionsBaseUrlOrThrowBadRequest = (options: Record<string, unknown>) => {
const parsedOptions = timetableOptionsSchema.safeParse(options);
if (!parsedOptions.success) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid timetable widget options" });
}
return normalizeBaseUrlOrThrowBadRequest(parsedOptions.data.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
};
export const validateTimetableOptionsChangeAsync = async (
nextOptions: Record<string, unknown>,
previousOptions?: Record<string, unknown>,
) => {
const nextBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(nextOptions);
if (nextBaseUrl === DEFAULT_TIMETABLE_BASE_URL) return;
let previousBaseUrl: string | undefined;
if (previousOptions) {
try {
previousBaseUrl = normalizeTimetableOptionsBaseUrlOrThrowBadRequest(previousOptions);
} catch {
// Invalid legacy options must not prevent replacing them with a valid configuration.
}
}
if (nextBaseUrl === previousBaseUrl) return;
await resolvePublicTimetableAddressesAsync(nextBaseUrl);
};
const getSavedTimetableBaseUrl = (serializedOptions: string) => {
const options = parseSavedTimetableOptions(serializedOptions);
return normalizeSavedTimetableBaseUrl(options.baseUrl ?? DEFAULT_TIMETABLE_BASE_URL);
@@ -174,6 +202,7 @@ const resolveTimetableBaseUrlAsync = async (
const requestedBaseUrl = normalizeBaseUrlOrThrowBadRequest(input.baseUrl);
if (allowBoardConfiguration) {
if (input.boardId) {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
let savedOptions: string | undefined;
if (input.itemId) {
const item = await ctx.db.query.items.findFirst({
@@ -182,8 +211,6 @@ const resolveTimetableBaseUrlAsync = async (
if (!item) throw new TRPCError({ code: "NOT_FOUND", message: "Timetable widget not found" });
savedOptions = item.options;
}
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
const savedBaseUrl = savedOptions === undefined ? undefined : getSavedTimetableBaseUrl(savedOptions);
if (requestedBaseUrl === savedBaseUrl || requestedBaseUrl === DEFAULT_TIMETABLE_BASE_URL) {
return { baseUrl: requestedBaseUrl };