chore(release): automatic release v1.19.0

This commit is contained in:
homarr-releases[bot]
2025-05-02 19:14:41 +00:00
committed by GitHub
101 changed files with 1197 additions and 1209 deletions

View File

@@ -48,21 +48,21 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.17.5",
"@mantine/core": "^7.17.5",
"@mantine/dropzone": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/modals": "^7.17.5",
"@mantine/tiptap": "^7.17.5",
"@mantine/colors-generator": "^7.17.7",
"@mantine/core": "^7.17.7",
"@mantine/dropzone": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/modals": "^7.17.7",
"@mantine/tiptap": "^7.17.7",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.4",
"@tanstack/react-query-next-experimental": "^5.74.4",
"@trpc/client": "^11.1.1",
"@trpc/next": "^11.1.1",
"@trpc/react-query": "^11.1.1",
"@trpc/server": "^11.1.1",
"@tanstack/react-query": "^5.75.1",
"@tanstack/react-query-devtools": "^5.75.1",
"@tanstack/react-query-next-experimental": "^5.75.1",
"@trpc/client": "^11.1.2",
"@trpc/next": "^11.1.2",
"@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -92,10 +92,10 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.15.2",
"@types/node": "^22.15.3",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/react-dom": "19.1.3",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
"eslint": "^9.25.1",

View File

@@ -4,15 +4,21 @@ import { TRPCError } from "@trpc/server";
// Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@homarr/api/server";
import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
import { isNullOrWhitespace } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { getI18n } from "@homarr/translation/server";
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
import { createMetaTitle } from "~/metadata";
import { createBoardLayout } from "../_layout-creator";
import type { Board } from "../_types";
import type { Board, Item } from "../_types";
import { DynamicClientBoard } from "./_dynamic-client";
import { BoardContentHeaderActions } from "./_header-actions";
@@ -31,14 +37,36 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
getInitialBoardAsync: getInitialBoard,
}),
// eslint-disable-next-line no-restricted-syntax
page: async () => {
page: async ({ params }: { params: Promise<TParams> }) => {
const session = await auth();
const integrations = await getIntegrationsWithPermissionsAsync(session);
const board = await getInitialBoard(await params);
const queryClient = getQueryClient();
// Prefetch item data
const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind);
if (existing) {
existing.push(item);
} else {
acc.set(item.kind, [item]);
}
return acc;
}, new Map<WidgetKind, Item[]>());
for (const [kind, items] of itemsMap) {
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(new Error("Failed to prefetch widget", { cause: error }));
});
}
return (
<IntegrationProvider integrations={integrations}>
<DynamicClientBoard />
</IntegrationProvider>
<HydrationBoundary state={dehydrate(queryClient)}>
<IntegrationProvider integrations={integrations}>
<DynamicClientBoard />
</IntegrationProvider>
</HydrationBoundary>
);
},
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {

View File

@@ -7,6 +7,7 @@ import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
import type { DayOfWeek } from "@mantine/dates";
import { NextIntlClientProvider } from "next-intl";
import { api } from "@homarr/api/server";
@@ -87,7 +88,15 @@ export default async function Layout(props: {
},
(innerProps) => (
<SettingsProvider
user={user}
user={
user
? {
...user,
// Convert type, because output schema is not smart enough to infer $type from drizzle
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
}
: null
}
serverSettings={{
board: {
homeBoardId: serverSettings.board.homeBoardId,

View File

@@ -1,5 +1,5 @@
import type { WidgetKind } from "@homarr/definitions";
import type { SettingsContextProps } from "@homarr/settings";
import type { SettingsContextProps } from "@homarr/settings/creator";
import type { WidgetComponentProps } from "@homarr/widgets";
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";

View File

@@ -44,11 +44,11 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.2",
"@types/node": "^22.15.3",
"dotenv-cli": "^8.0.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"tsx": "4.19.3",
"tsx": "4.19.4",
"typescript": "^5.8.3"
}
}

View File

@@ -26,7 +26,7 @@
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0",
"tsx": "4.19.3",
"tsx": "4.19.4",
"ws": "^8.18.1"
},
"devDependencies": {

View File

@@ -47,13 +47,13 @@
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"semantic-release": "^24.2.3",
"testcontainers": "^10.24.2",
"testcontainers": "^10.25.0",
"turbo": "^2.5.2",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2"
},
"packageManager": "pnpm@10.9.0",
"packageManager": "pnpm@10.10.0",
"engines": {
"node": ">=22.15.0"
},
@@ -72,9 +72,6 @@
"overrides": {
"proxmox-api>undici": "7.8.0"
},
"patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [
"@scarf/scarf",

View File

@@ -41,12 +41,13 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.2",
"@trpc/client": "^11.1.1",
"@trpc/react-query": "^11.1.1",
"@trpc/server": "^11.1.1",
"@tanstack/react-query": "^5.75.1",
"@trpc/client": "^11.1.2",
"@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2",
"@trpc/tanstack-react-query": "^11.1.2",
"lodash.clonedeep": "^4.5.0",
"next": "15.3.1",
"pretty-print-error": "^1.1.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",

View File

@@ -20,7 +20,7 @@ export type IntegrationAction = "query" | "interact";
* @param action query for showing data or interact for mutating data
* @param kinds kinds of integrations that are supported
* @returns middleware that can be used with trpc
* @example publicProcedure.unstable_concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @example publicProcedure.concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @throws TRPCError NOT_FOUND if the integration was not found
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on the specified integration
*/
@@ -82,7 +82,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
* @param action query for showing data or interact for mutating data
* @param kinds kinds of integrations that are supported
* @returns middleware that can be used with trpc
* @example publicProcedure.unstable_concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @example publicProcedure.concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @throws TRPCError NOT_FOUND if the integration was not found
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
*/

View File

@@ -18,7 +18,7 @@ const dockerCache = createCacheChannel<{
export const dockerRouter = createTRPCRouter({
getContainers: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.query(async () => {
const result = await dockerCache
.consumeAsync(async () => {
@@ -81,14 +81,14 @@ export const dockerRouter = createTRPCRouter({
}),
invalidate: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.mutation(async () => {
await dockerCache.invalidateAsync();
return;
}),
startAll: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => {
await Promise.allSettled(
@@ -102,7 +102,7 @@ export const dockerRouter = createTRPCRouter({
}),
stopAll: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => {
await Promise.allSettled(
@@ -116,7 +116,7 @@ export const dockerRouter = createTRPCRouter({
}),
restartAll: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => {
await Promise.allSettled(
@@ -130,7 +130,7 @@ export const dockerRouter = createTRPCRouter({
}),
removeAll: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(dockerMiddleware())
.concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => {
await Promise.allSettled(

View File

@@ -467,7 +467,7 @@ export const integrationRouter = createTRPCRouter({
});
}),
searchInIntegration: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
.input(z.object({ integrationId: z.string(), query: z.string() }))
.query(async ({ ctx, input }) => {
const integrationInstance = await createIntegrationAsync(ctx.integration);

View File

@@ -13,7 +13,7 @@ import { MemoryResourceParser } from "../resource-parser/memory-resource-parser"
export const clusterRouter = createTRPCRouter({
getCluster: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesCluster> => {
const { coreApi, metricsApi, versionApi, kubeConfig } = KubernetesClient.getInstance();

View File

@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const configMapsRouter = createTRPCRouter({
getConfigMaps: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesBaseResource[]> => {
const { coreApi } = KubernetesClient.getInstance();

View File

@@ -11,7 +11,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const ingressesRouter = createTRPCRouter({
getIngresses: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesIngress[]> => {
const { networkingApi } = KubernetesClient.getInstance();
try {

View File

@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const namespacesRouter = createTRPCRouter({
getNamespaces: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesNamespace[]> => {
const { coreApi } = KubernetesClient.getInstance();

View File

@@ -12,7 +12,7 @@ import { MemoryResourceParser } from "../resource-parser/memory-resource-parser"
export const nodesRouter = createTRPCRouter({
getNodes: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesNode[]> => {
const { coreApi, metricsApi } = KubernetesClient.getInstance();

View File

@@ -12,7 +12,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const podsRouter = createTRPCRouter({
getPods: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesPod[]> => {
const { coreApi, kubeConfig } = KubernetesClient.getInstance();
try {

View File

@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const secretsRouter = createTRPCRouter({
getSecrets: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesSecret[]> => {
const { coreApi } = KubernetesClient.getInstance();
try {

View File

@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const servicesRouter = createTRPCRouter({
getServices: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesService[]> => {
const { coreApi } = KubernetesClient.getInstance();

View File

@@ -10,7 +10,7 @@ import { KubernetesClient } from "../kubernetes-client";
export const volumesRouter = createTRPCRouter({
getVolumes: permissionRequiredProcedure
.requiresPermission("admin")
.unstable_concat(kubernetesMiddleware())
.concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesVolume[]> => {
const { coreApi } = KubernetesClient.getInstance();

View File

@@ -133,14 +133,14 @@ export const searchEngineRouter = createTRPCRouter({
});
}),
getMediaRequestOptions: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(mediaRequestOptionsSchema)
.query(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}),
requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(mediaRequestRequestSchema)
.mutation(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration);

View File

@@ -17,7 +17,7 @@ export const calendarRouter = createTRPCRouter({
showUnmonitored: z.boolean(),
}),
)
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
.query(async ({ ctx, input }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {

View File

@@ -14,7 +14,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trp
export const dnsHoleRouter = createTRPCRouter({
summary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -36,7 +36,7 @@ export const dnsHoleRouter = createTRPCRouter({
}),
subscribeToSummary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"dnsHole"> }>;
@@ -63,7 +63,7 @@ export const dnsHoleRouter = createTRPCRouter({
}),
enable: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration } }) => {
const client = await createIntegrationAsync(integration);
await client.enableAsync();
@@ -81,7 +81,7 @@ export const dnsHoleRouter = createTRPCRouter({
duration: z.number().optional(),
}),
)
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);
await client.disableAsync(input.duration);

View File

@@ -18,7 +18,7 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
export const downloadsRouter = createTRPCRouter({
getJobsAndStatuses: publicProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
.concat(createDownloadClientIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -39,7 +39,7 @@ export const downloadsRouter = createTRPCRouter({
);
}),
subscribeToJobsAndStatuses: publicProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
.concat(createDownloadClientIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
@@ -64,18 +64,16 @@ export const downloadsRouter = createTRPCRouter({
};
});
}),
pause: protectedProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.pauseQueueAsync();
}),
);
}),
pause: protectedProcedure.concat(createDownloadClientIntegrationMiddleware("interact")).mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.pauseQueueAsync();
}),
);
}),
pauseItem: protectedProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
.concat(createDownloadClientIntegrationMiddleware("interact"))
.input(z.object({ item: downloadClientItemSchema }))
.mutation(async ({ ctx, input }) => {
await Promise.all(
@@ -85,18 +83,16 @@ export const downloadsRouter = createTRPCRouter({
}),
);
}),
resume: protectedProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.resumeQueueAsync();
}),
);
}),
resume: protectedProcedure.concat(createDownloadClientIntegrationMiddleware("interact")).mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = await createIntegrationAsync(integration);
await integrationInstance.resumeQueueAsync();
}),
);
}),
resumeItem: protectedProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
.concat(createDownloadClientIntegrationMiddleware("interact"))
.input(z.object({ item: downloadClientItemSchema }))
.mutation(async ({ ctx, input }) => {
await Promise.all(
@@ -107,7 +103,7 @@ export const downloadsRouter = createTRPCRouter({
);
}),
deleteItem: protectedProcedure
.unstable_concat(createDownloadClientIntegrationMiddleware("interact"))
.concat(createDownloadClientIntegrationMiddleware("interact"))
.input(z.object({ item: downloadClientItemSchema, fromDisk: z.boolean() }))
.mutation(async ({ ctx, input }) => {
await Promise.all(

View File

@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
);
}),
subscribeSystemHealthStatus: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = [];
@@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({
});
}),
getClusterHealthStatus: publicProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
.concat(createOneIntegrationMiddleware("query", "proxmox"))
.query(async ({ ctx }) => {
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data;
}),
subscribeClusterHealthStatus: publicProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
.concat(createOneIntegrationMiddleware("query", "proxmox"))
.subscription(({ ctx }) => {
return observable<ProxmoxClusterInfo>((emit) => {
const unsubscribes: (() => void)[] = [];

View File

@@ -16,7 +16,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
export const indexerManagerRouter = createTRPCRouter({
getIndexersStatus: publicProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
.concat(createIndexerManagerIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -33,7 +33,7 @@ export const indexerManagerRouter = createTRPCRouter({
}),
subscribeIndexersStatus: publicProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
.concat(createIndexerManagerIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; indexers: Indexer[] }>((emit) => {
const unsubscribes: (() => void)[] = [];
@@ -55,7 +55,7 @@ export const indexerManagerRouter = createTRPCRouter({
});
}),
testAllIndexers: protectedProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("interact"))
.concat(createIndexerManagerIntegrationMiddleware("interact"))
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {

View File

@@ -12,7 +12,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trp
export const mediaRequestsRouter = createTRPCRouter({
getLatestRequests: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -39,7 +39,7 @@ export const mediaRequestsRouter = createTRPCRouter({
});
}),
subscribeToLatestRequests: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.subscription(({ ctx }) => {
return observable<{
integrationId: string;
@@ -65,7 +65,7 @@ export const mediaRequestsRouter = createTRPCRouter({
});
}),
getStats: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -91,7 +91,7 @@ export const mediaRequestsRouter = createTRPCRouter({
};
}),
answerRequest: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
.concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
.input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) }))
.mutation(async ({ ctx: { integration }, input }) => {
const integrationInstance = await createIntegrationAsync(integration);

View File

@@ -14,7 +14,7 @@ const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
export const mediaServerRouter = createTRPCRouter({
getCurrentStreams: publicProcedure
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
.concat(createMediaServerIntegrationMiddleware("query"))
.input(z.object({ showOnlyPlaying: z.boolean() }))
.query(async ({ ctx, input }) => {
return await Promise.all(
@@ -32,7 +32,7 @@ export const mediaServerRouter = createTRPCRouter({
);
}),
subscribeToCurrentStreams: publicProcedure
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
.concat(createMediaServerIntegrationMiddleware("query"))
.input(z.object({ showOnlyPlaying: z.boolean() }))
.subscription(({ ctx, input }) => {
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {

View File

@@ -11,7 +11,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
export const mediaTranscodingRouter = createTRPCRouter({
getDataAsync: publicProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
.concat(createIndexerManagerIntegrationMiddleware("query"))
.input(paginatedSchema.pick({ page: true, pageSize: true }))
.query(async ({ ctx, input }) => {
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {

View File

@@ -12,7 +12,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const networkControllerRouter = createTRPCRouter({
summary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
@@ -34,7 +34,7 @@ export const networkControllerRouter = createTRPCRouter({
}),
subscribeToSummary: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"networkController"> }>;

View File

@@ -15,14 +15,14 @@ const createSmartHomeIntegrationMiddleware = (action: IntegrationAction) =>
export const smartHomeRouter = createTRPCRouter({
entityState: publicProcedure
.input(z.object({ entityId: z.string() }))
.unstable_concat(createSmartHomeIntegrationMiddleware("query"))
.concat(createSmartHomeIntegrationMiddleware("query"))
.query(async ({ ctx: { integration }, input }) => {
const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId });
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data;
}),
subscribeEntityState: publicProcedure
.unstable_concat(createSmartHomeIntegrationMiddleware("query"))
.concat(createSmartHomeIntegrationMiddleware("query"))
.input(z.object({ entityId: z.string() }))
.subscription(({ input, ctx }) => {
return observable<{
@@ -42,7 +42,7 @@ export const smartHomeRouter = createTRPCRouter({
});
}),
switchEntity: protectedProcedure
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ entityId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);
@@ -54,7 +54,7 @@ export const smartHomeRouter = createTRPCRouter({
return success;
}),
executeAutomation: protectedProcedure
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
.concat(createSmartHomeIntegrationMiddleware("interact"))
.input(z.object({ automationId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);

View File

@@ -1,9 +1,12 @@
import { cache } from "react";
import { headers } from "next/headers";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import { createCaller, createTRPCContext } from "@homarr/api";
import { appRouter, createCaller, createTRPCContext } from "@homarr/api";
import { auth } from "@homarr/auth/next";
import { makeQueryClient } from "./shared";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a tRPC call from a React Server Component.
@@ -19,3 +22,12 @@ const createContext = cache(async () => {
});
export const api = createCaller(createContext);
// IMPORTANT: Create a stable getter for the query client that
// will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
ctx: createContext,
router: appRouter,
queryClient: getQueryClient,
});

View File

@@ -1,3 +1,5 @@
import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query";
/**
* Creates a headers callback for a given source
* It will set the x-trpc-source header and cookies if needed
@@ -51,3 +53,16 @@ export const trpcPath = "/api/trpc";
export function getTrpcUrl() {
return `${getBaseUrl()}${trpcPath}`;
}
export const makeQueryClient = () => {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
dehydrate: {
shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending",
},
},
});
};

View File

@@ -37,7 +37,6 @@
"ldapts": "7.4.0",
"next": "15.3.1",
"next-auth": "5.0.0-beta.27",
"pretty-print-error": "^1.1.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.24.3"

View File

@@ -1,22 +1,8 @@
import type { CookieSerializeOptions } from "cookie";
import { serialize } from "cookie";
import { parse, serialize } from "cookie";
export function parseCookies(cookieString: string) {
const list: Record<string, string> = {};
const cookieHeader = cookieString;
if (!cookieHeader) return list;
cookieHeader.split(";").forEach(function (cookie) {
const items = cookie.split("=");
let name = items.shift();
name = name?.trim();
if (!name) return;
const value = items.join("=").trim();
if (!value) return;
list[name] = decodeURIComponent(value);
});
return list;
return parse(cookieString);
}
export function setClientCookie(name: string, value: string, options: CookieSerializeOptions = {}) {

View File

@@ -0,0 +1,40 @@
import { describe, expect, test } from "vitest";
import { getPortFromUrl } from "../url";
describe("getPortFromUrl", () => {
test.each([
[80, "http"],
[443, "https"],
])("should return %s for %s protocol without port", (expectedPort, protocol) => {
// Arrange
const url = new URL(`${protocol}://example.com`);
// Act
const port = getPortFromUrl(url);
// Assert
expect(port).toBe(expectedPort);
});
test.each([["http"], ["https"], ["anything"]])("should return the specified port for %s protocol", (protocol) => {
// Arrange
const expectedPort = 3000;
const url = new URL(`${protocol}://example.com:${expectedPort}`);
// Act
const port = getPortFromUrl(url);
// Assert
expect(port).toBe(expectedPort);
});
test("should throw an error for unsupported protocol", () => {
// Arrange
const url = new URL("ftp://example.com");
// Act
const act = () => getPortFromUrl(url);
// Act & Assert
expect(act).toThrowError("Unsupported protocol: ftp:");
});
});

View File

@@ -21,3 +21,20 @@ export const extractBaseUrlFromHeaders = (
return `${protocol}://${host}`;
};
export const getPortFromUrl = (url: URL): number => {
const port = url.port;
if (port) {
return Number(port);
}
if (url.protocol === "https:") {
return 443;
}
if (url.protocol === "http:") {
return 80;
}
throw new Error(`Unsupported protocol: ${url.protocol}`);
};

View File

@@ -44,9 +44,9 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/core": "^7.17.7",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.24.2",
"@testcontainers/mysql": "^10.25.0",
"better-sqlite3": "^11.9.1",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.0",
@@ -62,7 +62,7 @@
"dotenv-cli": "^8.0.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"tsx": "4.19.3",
"tsx": "4.19.4",
"typescript": "^5.8.3"
}
}

View File

@@ -113,6 +113,8 @@ export type HomarrDocumentationPath =
| "/docs/tags/proxmox"
| "/docs/tags/proxy"
| "/docs/tags/puid"
| "/docs/tags/releases"
| "/docs/tags/repositories"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/rss"
@@ -212,6 +214,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/media-server"
| "/docs/widgets/minecraft-server-status"
| "/docs/widgets/notebook"
| "/docs/widgets/releases"
| "/docs/widgets/rss"
| "/docs/widgets/stocks"
| "/docs/widgets/video"

View File

@@ -23,7 +23,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.0",
"@t3-oss/env-nextjs": "^0.13.4",
"zod": "^3.24.3"
},
"devDependencies": {

View File

@@ -26,7 +26,7 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^7.17.5",
"@mantine/form": "^7.17.7",
"zod": "^3.24.3"
},
"devDependencies": {

View File

@@ -29,7 +29,7 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/core": "^7.17.7",
"react": "19.1.0",
"zod": "^3.24.3"
},

View File

@@ -48,6 +48,7 @@ export enum MediaRequestStatus {
Approved = 2,
Declined = 3,
Failed = 4,
Completed = 5,
}
export enum MediaAvailability {

View File

@@ -1,6 +1,8 @@
import type { SiteStats } from "node-unifi";
import { Controller } from "node-unifi";
import { getPortFromUrl } from "@homarr/common";
import { Integration } from "../base/integration";
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
@@ -42,20 +44,16 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
}
private async createControllerClientAsync() {
const portString = new URL(this.integration.url).port;
const port = Number.isInteger(portString) ? Number(portString) : undefined;
const hostname = new URL(this.integration.url).hostname;
const url = new URL(this.integration.url);
const client = new Controller({
host: hostname,
// @ts-expect-error the URL construction is incorrect and does not append the required / at the end: https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L56, https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L95
port: port === undefined ? "/" : `${port}/`,
host: url.hostname,
port: getPortFromUrl(url),
sslverify: false, // TODO: implement a "ignore certificate toggle", see https://github.com/homarr-labs/homarr/issues/2553
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
});
// Object.defineProperty(client, '_baseurl', { value: url });
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
return client;
}

View File

@@ -33,7 +33,7 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/core": "^7.17.7",
"@tabler/icons-react": "^3.31.0",
"dayjs": "^1.11.13",
"next": "15.3.1",

View File

@@ -24,8 +24,8 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"react": "19.1.0"
},
"devDependencies": {

View File

@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^7.17.5",
"@mantine/notifications": "^7.17.7",
"@tabler/icons-react": "^3.31.0"
},
"devDependencies": {

View File

@@ -37,8 +37,8 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"adm-zip": "0.5.16",
"next": "15.3.1",
"react": "19.1.0",

View File

@@ -24,8 +24,7 @@
"dependencies": {
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"pretty-print-error": "^1.1.2"
"@homarr/log": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -31,7 +31,6 @@
"@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"octokit": "^4.1.3",
"pretty-print-error": "^1.1.2",
"superjson": "2.2.2"
},
"devDependencies": {

View File

@@ -5,7 +5,8 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
".": "./index.ts",
"./creator": "./src/creator.ts"
},
"typesVersions": {
"*": {
@@ -25,7 +26,7 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^7.17.5",
"@mantine/dates": "^7.17.7",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0"

View File

@@ -2,30 +2,9 @@
import type { PropsWithChildren } from "react";
import { createContext, useContext } from "react";
import type { DayOfWeek } from "@mantine/dates";
import type { RouterOutputs } from "@homarr/api";
import type { User } from "@homarr/db/schema";
import type { ServerSettings } from "@homarr/server-settings";
export type SettingsContextProps = Pick<
User,
| "firstDayOfWeek"
| "defaultSearchEngineId"
| "homeBoardId"
| "mobileHomeBoardId"
| "openSearchInNewTab"
| "pingIconsEnabled"
> &
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
interface PublicServerSettings {
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
board: Pick<
ServerSettings["board"],
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
>;
}
import type { PublicServerSettings, SettingsContextProps, UserSettings } from "./creator";
import { createSettings } from "./creator";
const SettingsContext = createContext<SettingsContextProps | null>(null);
@@ -33,22 +12,9 @@ export const SettingsProvider = ({
user,
serverSettings,
children,
}: PropsWithChildren<{ user: RouterOutputs["user"]["getById"] | null; serverSettings: PublicServerSettings }>) => {
}: PropsWithChildren<{ user: UserSettings | null; serverSettings: PublicServerSettings }>) => {
return (
<SettingsContext.Provider
value={{
defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId,
openSearchInNewTab: user?.openSearchInNewTab ?? true,
firstDayOfWeek: (user?.firstDayOfWeek as DayOfWeek | undefined) ?? (1 as const),
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
pingIconsEnabled: user?.pingIconsEnabled ?? false,
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
}}
>
{children}
</SettingsContext.Provider>
<SettingsContext.Provider value={createSettings({ user, serverSettings })}>{children}</SettingsContext.Provider>
);
};

View File

@@ -0,0 +1,48 @@
import type { User } from "@homarr/db/schema";
import type { ServerSettings } from "@homarr/server-settings";
export type SettingsContextProps = Pick<
User,
| "firstDayOfWeek"
| "defaultSearchEngineId"
| "homeBoardId"
| "mobileHomeBoardId"
| "openSearchInNewTab"
| "pingIconsEnabled"
> &
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
export interface PublicServerSettings {
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
board: Pick<
ServerSettings["board"],
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
>;
}
export type UserSettings = Pick<
User,
| "firstDayOfWeek"
| "defaultSearchEngineId"
| "homeBoardId"
| "mobileHomeBoardId"
| "openSearchInNewTab"
| "pingIconsEnabled"
>;
export const createSettings = ({
user,
serverSettings,
}: {
user: UserSettings | null;
serverSettings: PublicServerSettings;
}) => ({
defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId,
openSearchInNewTab: user?.openSearchInNewTab ?? true,
firstDayOfWeek: user?.firstDayOfWeek ?? (1 as const),
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
pingIconsEnabled: user?.pingIconsEnabled ?? false,
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
});

View File

@@ -33,9 +33,9 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/spotlight": "^7.17.5",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/spotlight": "^7.17.7",
"@tabler/icons-react": "^3.31.0",
"jotai": "^2.12.3",
"next": "15.3.1",

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "待处理",
"approved": "已批准",
"declined": "已拒绝",
"failed": "失败"
"failed": "失败",
"completed": ""
},
"toBeDetermined": "待定"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "Afventende",
"approved": "Godkendt",
"declined": "Afvist",
"failed": "Mislykket"
"failed": "Mislykket",
"completed": ""
},
"toBeDetermined": "TBD"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "Ausstehend",
"approved": "Bestätigt",
"declined": "Abgelehnt",
"failed": "Fehlgeschlagen"
"failed": "Fehlgeschlagen",
"completed": ""
},
"toBeDetermined": "Noch Festzulegen"
},

View File

@@ -618,11 +618,11 @@
"title": ""
},
"create": {
"title": "",
"title": "Neue App erstellen",
"description": "",
"action": ""
},
"add": ""
"add": "App hinzufügen"
}
},
"integration": {
@@ -995,7 +995,7 @@
},
"option": {
"title": {
"label": ""
"label": "Titel"
},
"borderColor": {
"label": "Rahmenfarbe"
@@ -1949,7 +1949,8 @@
"pending": "Ausstehend",
"approved": "Bestätigt",
"declined": "Abgelehnt",
"failed": "Fehlgeschlagen"
"failed": "Fehlgeschlagen",
"completed": ""
},
"toBeDetermined": "Noch Festzulegen"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "Pending",
"approved": "Approved",
"declined": "Declined",
"failed": "Failed"
"failed": "Failed",
"completed": "Completed"
},
"toBeDetermined": "TBD"
},

View File

@@ -3,7 +3,7 @@
"step": {
"start": {
"title": "Bienvenido a Homarr",
"subtitle": "",
"subtitle": "Comencemos con la configuración de tu instancia de Homar.",
"description": "Para empezar, por favor, seleccione cómo desea configurar su instancia de Homarr.",
"action": {
"scratch": "Empezar de cero",
@@ -27,18 +27,18 @@
"description": ""
},
"boardSelection": {
"title": "",
"title": "Se encontraron {count} tableros",
"description": "",
"action": {
"selectAll": "",
"unselectAll": ""
"selectAll": "Seleccionar todo",
"unselectAll": "Deseleccionar todo"
}
},
"summary": {
"title": "Importar resumen",
"description": "",
"action": {
"import": ""
"import": "Confirmar la importación y continuar"
},
"entities": {
"apps": "Aplicaciones",
@@ -78,24 +78,24 @@
},
"group": {
"title": "",
"subtitle": "",
"subtitle": "Especifique el grupo que debe ser usado para usuarios externos.",
"form": {
"name": {
"label": "",
"description": ""
"label": "Nombre del grupo",
"description": "El nombre debe coincidir con el grupo de administración del proveedor externo"
}
}
},
"settings": {
"title": "Ajustes",
"subtitle": ""
"subtitle": "Configurar ajustes del servidor."
},
"finish": {
"title": "",
"subtitle": "",
"description": "",
"action": {
"goToBoard": "",
"goToBoard": "Ir al tablero {name}",
"createBoard": "",
"inviteUser": "",
"docs": ""
@@ -109,7 +109,7 @@
"name": "Usuario",
"page": {
"login": {
"title": "",
"title": "Inicia sesión en tu cuenta",
"subtitle": ""
},
"invite": {
@@ -160,12 +160,12 @@
}
},
"error": {
"usernameTaken": ""
"usernameTaken": "Nombre de usuario ya está ocupado"
},
"action": {
"login": {
"label": "Iniciar sesión",
"labelWith": "",
"labelWith": "Iniciar sesión con {provider}",
"notification": {
"success": {
"title": "",
@@ -229,10 +229,10 @@
"changeFirstDayOfWeek": {
"notification": {
"success": {
"message": ""
"message": "Primer día de la semana cambiado con éxito"
},
"error": {
"message": ""
"message": "No se ha podido cambiar el primer día de la semana"
}
}
},
@@ -251,7 +251,7 @@
"label": "",
"notification": {
"success": {
"message": ""
"message": "La imagen ha sido cambiada con éxito"
},
"error": {
"message": "No se puede cambiar la imagen"
@@ -267,7 +267,7 @@
"confirm": "Por favor, confirma que deseas eliminar esta imagen",
"notification": {
"success": {
"message": ""
"message": "Imagen eliminada con éxito"
},
"error": {
"message": ""
@@ -278,7 +278,7 @@
"editProfile": {
"notification": {
"success": {
"message": ""
"message": "Perfil actualizado con éxito"
},
"error": {
"message": ""
@@ -288,10 +288,10 @@
"delete": {
"label": "",
"description": "",
"confirm": ""
"confirm": "¿Estás seguro de que quieres eliminar el usuario {username} con sus preferencias?"
},
"select": {
"label": "",
"label": "Seleccionar usuario",
"notFound": ""
},
"transfer": {
@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "En attente",
"approved": "Approuvé",
"declined": "Refusé",
"failed": "Échec"
"failed": "Échec",
"completed": ""
},
"toBeDetermined": "À déterminer"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "ממתין",
"approved": "אושר",
"declined": "נדחה",
"failed": "נכשל"
"failed": "נכשל",
"completed": ""
},
"toBeDetermined": "ייקבע בהמשך"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "In afwachting",
"approved": "Goedgekeurd",
"declined": "Afgewezen",
"failed": "Mislukt"
"failed": "Mislukt",
"completed": ""
},
"toBeDetermined": "TBD"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "Venter",
"approved": "Godkjent",
"declined": "Avslått",
"failed": "Feilet"
"failed": "Feilet",
"completed": ""
},
"toBeDetermined": "Uavklart"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "В ожидании",
"approved": "Одобрено",
"declined": "Отклонено",
"failed": "Ошибка"
"failed": "Ошибка",
"completed": ""
},
"toBeDetermined": "Будет определено позже"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": "TBD"
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "Bekleyen",
"approved": "Onaylandı",
"declined": "Reddedildi",
"failed": "Başarısız"
"failed": "Başarısız",
"completed": ""
},
"toBeDetermined": "-Yapım Aşamasında-"
},
@@ -2053,83 +2054,83 @@
}
},
"releases": {
"name": "",
"description": "",
"name": "Sürümler",
"description": "Verilen sürüm düzenli ifadesiyle, belirtilen depoların mevcut sürümlerinin bir listesini gösterir.",
"option": {
"newReleaseWithin": {
"label": "",
"description": ""
"label": "Yeni Sürüm Süresi (içinde)",
"description": "Kullanım örneği: 1w (1 hafta), 10m (10 ay). Kabul edilen birim türleri: h (saat), d (gün), w (hafta), m (ay), y (yıl). Yeni sürümleri vurgulamak istemiyorsanız boş bırakın."
},
"staleReleaseWithin": {
"label": "",
"description": ""
"label": "Eski Sürüm Süresi (içinde)",
"description": "Kullanım örneği: 1w (1 hafta), 10m (10 ay). Kabul edilen birim türleri h (saat), d (gün), w (hafta), m (ay), y (yıl). Eski sürümleri vurgulamak istemiyorsanız boş bırakın."
},
"showOnlyHighlighted": {
"label": "",
"description": ""
"label": "Yalnızca Vurgulananları Göster",
"description": "Yalnızca yeni veya eski sürümleri göster. Yukarıdaki açıklamaya göre."
},
"showDetails": {
"label": ""
"label": "Ayrıntıları Göster"
},
"repositories": {
"label": "",
"label": "Depolar",
"addRRepository": {
"label": ""
"label": "Depo Ekle"
},
"provider": {
"label": ""
"label": "Sağlayıcı"
},
"identifier": {
"label": "",
"placeholder": ""
"label": "Tanımlayıcı",
"placeholder": "Ad veya Sahip/Ad"
},
"versionFilter": {
"label": "",
"label": "Sürüm Filtresi",
"prefix": {
"label": ""
"label": "Önek"
},
"precision": {
"label": "",
"label": "Hassasiyet",
"options": {
"none": ""
"none": "Hiçbiri"
}
},
"suffix": {
"label": ""
"label": "Sonek"
},
"regex": {
"label": ""
"label": "Düzenli İfade"
}
},
"edit": {
"label": ""
"label": "Düzenle"
},
"editForm": {
"title": "",
"title": "Depoyu Düzenle",
"cancel": {
"label": ""
"label": "Vazgeç"
},
"confirm": {
"label": ""
"label": "Onayla"
}
},
"example": {
"label": ""
"label": "Örnek"
},
"invalid": ""
"invalid": "Geçersiz depo tanımı, lütfen değerleri kontrol edin"
}
},
"not-found": "",
"pre-release": "",
"archived": "",
"forked": "",
"starsCount": "",
"forksCount": "",
"issuesCount": "",
"openProjectPage": "",
"openReleasePage": "",
"releaseDescription": "",
"created": ""
"not-found": "Bulunamadı",
"pre-release": "Ön Sürüm",
"archived": "Arşivlenmiş",
"forked": "Çatallanmış",
"starsCount": "Yıldızlar",
"forksCount": "Çatallar",
"issuesCount": "Sorunları",
"openProjectPage": "Proje Sayfasını",
"openReleasePage": "Sürüm Sayfasını",
"releaseDescription": "Sürüm Açıklaması",
"created": "Oluşturuldu"
},
"networkControllerSummary": {
"option": {},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1949,7 +1949,8 @@
"pending": "",
"approved": "",
"declined": "",
"failed": ""
"failed": "",
"completed": ""
},
"toBeDetermined": ""
},

View File

@@ -1093,7 +1093,7 @@
"label": "集成"
},
"title": {
"label": ""
"label": "標題"
},
"customCssClasses": {
"label": "自定義 CSS html"
@@ -1771,8 +1771,8 @@
"description": "顯示當前多媒體伺服器的串流",
"option": {
"showOnlyPlaying": {
"label": "",
"description": ""
"label": "只顯示目前播放中的項目",
"description": "停用此功能將無法在 Plex 上運作"
}
},
"items": {
@@ -1949,7 +1949,8 @@
"pending": "待處理",
"approved": "已批准",
"declined": "已拒絕",
"failed": "失敗"
"failed": "失敗",
"completed": "已完成"
},
"toBeDetermined": "多媒體請求狀態"
},
@@ -2053,83 +2054,83 @@
}
},
"releases": {
"name": "",
"description": "",
"name": "版本發佈",
"description": "顯示符合指定版本規則的儲存庫及其目前版本列表",
"option": {
"newReleaseWithin": {
"label": "",
"description": ""
"label": "新版本發布於",
"description": "使用範例1w1週、10m10個月。接受的單位類型有h小時、d、w、m個月、y。若留空則不會特別標示新版本"
},
"staleReleaseWithin": {
"label": "",
"description": ""
"label": "過時版本範圍內",
"description": "使用範例1w1週、10m10個月。接受的單位類型有h小時、d、w、m個月、y。若留空則不會特別標示過時版本"
},
"showOnlyHighlighted": {
"label": "",
"description": ""
"label": "只顯示已標示的項目",
"description": "只顯示新版本或過時版本。如上所述"
},
"showDetails": {
"label": ""
"label": "顯示詳情"
},
"repositories": {
"label": "",
"label": "儲存庫",
"addRRepository": {
"label": ""
"label": "新增儲存庫"
},
"provider": {
"label": ""
"label": "提供者"
},
"identifier": {
"label": "",
"placeholder": ""
"label": "識別碼",
"placeholder": "名稱或擁有者/名稱"
},
"versionFilter": {
"label": "",
"label": "版本篩選",
"prefix": {
"label": ""
"label": "前綴"
},
"precision": {
"label": "",
"label": "精確度",
"options": {
"none": ""
"none": ""
}
},
"suffix": {
"label": ""
"label": "後綴"
},
"regex": {
"label": ""
"label": "正則表達式"
}
},
"edit": {
"label": ""
"label": "編輯"
},
"editForm": {
"title": "",
"title": "編輯儲存庫",
"cancel": {
"label": ""
"label": "取消"
},
"confirm": {
"label": ""
"label": "確認"
}
},
"example": {
"label": ""
"label": "範例"
},
"invalid": ""
"invalid": "無效的儲存庫定義,請檢查值的設定"
}
},
"not-found": "",
"pre-release": "",
"archived": "",
"forked": "",
"starsCount": "",
"forksCount": "",
"issuesCount": "",
"openProjectPage": "",
"openReleasePage": "",
"releaseDescription": "",
"created": ""
"not-found": "未找到",
"pre-release": "預發佈",
"archived": "已歸檔",
"forked": "已分支",
"starsCount": "星標",
"forksCount": "分支數",
"issuesCount": "開放問題",
"openProjectPage": "開啟專案頁面",
"openReleasePage": "開啟發布葉面",
"releaseDescription": "版本描述",
"created": "已創建"
},
"networkControllerSummary": {
"option": {},
@@ -3888,8 +3889,8 @@
"title": "尚無憑證"
},
"invalid": {
"title": "",
"description": ""
"title": "無效的證書",
"description": "解析證書失敗"
},
"expires": "到期 {when}"
}

View File

@@ -29,9 +29,9 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.5",
"@mantine/dates": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/core": "^7.17.7",
"@mantine/dates": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@tabler/icons-react": "^3.31.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1",

View File

@@ -7,7 +7,8 @@
"exports": {
".": "./index.ts",
"./errors": "./src/errors/component.tsx",
"./modals": "./src/modals/index.ts"
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
"typesVersions": {
"*": {
@@ -35,6 +36,7 @@
"@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/modals-collection": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
@@ -45,25 +47,25 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^7.17.5",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/charts": "^7.17.7",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@tabler/icons-react": "^3.31.0",
"@tiptap/extension-color": "2.11.7",
"@tiptap/extension-highlight": "2.11.7",
"@tiptap/extension-image": "2.11.7",
"@tiptap/extension-link": "^2.11.7",
"@tiptap/extension-table": "2.11.7",
"@tiptap/extension-table-cell": "2.11.7",
"@tiptap/extension-table-header": "2.11.7",
"@tiptap/extension-table-row": "2.11.7",
"@tiptap/extension-task-item": "2.11.7",
"@tiptap/extension-task-list": "2.11.7",
"@tiptap/extension-text-align": "2.11.7",
"@tiptap/extension-text-style": "2.11.7",
"@tiptap/extension-underline": "2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@tiptap/extension-color": "2.11.9",
"@tiptap/extension-highlight": "2.11.9",
"@tiptap/extension-image": "2.11.9",
"@tiptap/extension-link": "^2.11.9",
"@tiptap/extension-table": "2.11.9",
"@tiptap/extension-table-cell": "2.11.9",
"@tiptap/extension-table-header": "2.11.9",
"@tiptap/extension-table-row": "2.11.9",
"@tiptap/extension-task-item": "2.11.9",
"@tiptap/extension-task-list": "2.11.9",
"@tiptap/extension-text-align": "2.11.9",
"@tiptap/extension-text-style": "2.11.9",
"@tiptap/extension-underline": "2.11.9",
"@tiptap/react": "^2.11.9",
"@tiptap/starter-kit": "^2.11.9",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9",

View File

@@ -0,0 +1,23 @@
import { trpc } from "@homarr/api/server";
import { db, inArray } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { Prefetch } from "../definition";
const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => {
const appIds = items.map((item) => item.options.appId);
const distinctAppIds = [...new Set(appIds)];
const dbApps = await db.query.apps.findMany({
where: inArray(apps.id, distinctAppIds),
});
for (const app of dbApps) {
queryClient.setQueryData(trpc.app.byId.queryKey({ id: app.id }), app);
}
logger.info(`Successfully prefetched ${dbApps.length} apps for app widget`);
};
export default prefetchAllAsync;

View File

@@ -0,0 +1,30 @@
import { trpc } from "@homarr/api/server";
import { db, inArray } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { Prefetch } from "../definition";
const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => {
const appIds = items.flatMap((item) => item.options.items);
const distinctAppIds = [...new Set(appIds)];
const dbApps = await db.query.apps.findMany({
where: inArray(apps.id, distinctAppIds),
});
for (const item of items) {
if (item.options.items.length === 0) {
continue;
}
queryClient.setQueryData(
trpc.app.byIds.queryKey(item.options.items),
dbApps.filter((app) => item.options.items.includes(app.id)),
);
}
logger.info(`Successfully prefetched ${dbApps.length} apps for bookmarks`);
};
export default prefetchAllAsync;

View File

@@ -1,9 +1,10 @@
import type { LoaderComponent } from "next/dynamic";
import type { QueryClient } from "@tanstack/react-query";
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import type { ServerSettings } from "@homarr/server-settings";
import type { SettingsContextProps } from "@homarr/settings";
import type { SettingsContextProps } from "@homarr/settings/creator";
import type { stringOrTranslation } from "@homarr/translation";
import type { TablerIcon } from "@homarr/ui";
@@ -21,6 +22,15 @@ const createWithDynamicImport =
componentLoader,
});
export type PrefetchLoader<TKind extends WidgetKind> = () => Promise<{ default: Prefetch<TKind> }>;
export type Prefetch<TKind extends WidgetKind> = (
queryClient: QueryClient,
items: {
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
integrationIds: string[];
}[],
) => Promise<void>;
export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition extends WidgetDefinition>(
kind: TKind,
definition: TDefinition,

View File

@@ -5,7 +5,7 @@ import { Center, Loader as UiLoader } from "@mantine/core";
import { objectEntries } from "@homarr/common";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import type { SettingsContextProps } from "@homarr/settings";
import type { SettingsContextProps } from "@homarr/settings/creator";
import * as app from "./app";
import * as bookmarks from "./bookmarks";

View File

@@ -215,6 +215,7 @@ const statusMapping = {
[MediaRequestStatus.Approved]: { color: "green", label: (t) => t("approved") },
[MediaRequestStatus.Declined]: { color: "red", label: (t) => t("declined") },
[MediaRequestStatus.Failed]: { color: "red", label: (t) => t("failed") },
[MediaRequestStatus.Completed]: { color: "green", label: (t) => t("completed") },
} satisfies Record<
MediaRequestStatus,
{

View File

@@ -8,7 +8,7 @@ import { objectEntries } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions";
import { zodResolver } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import type { SettingsContextProps } from "@homarr/settings";
import type { SettingsContextProps } from "@homarr/settings/creator";
import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form/i18n";

View File

@@ -0,0 +1,45 @@
import { cache } from "react";
import type { QueryClient } from "@tanstack/react-query";
import { db } from "@homarr/db";
import { getServerSettingsAsync } from "@homarr/db/queries";
import type { WidgetKind } from "@homarr/definitions";
import { createSettings } from "@homarr/settings/creator";
import { reduceWidgetOptionsWithDefaultValues } from ".";
import prefetchForApps from "./app/prefetch";
import prefetchForBookmarks from "./bookmarks/prefetch";
import type { Prefetch, WidgetOptionsRecordOf } from "./definition";
import type { inferOptionsFromCreator } from "./options";
const cachedGetServerSettingsAsync = cache(getServerSettingsAsync);
const prefetchCallbacks: Partial<{
[TKind in WidgetKind]: Prefetch<TKind>;
}> = {
bookmarks: prefetchForBookmarks,
app: prefetchForApps,
};
export const prefetchForKindAsync = async <TKind extends WidgetKind>(
kind: TKind,
queryClient: QueryClient,
items: {
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
integrationIds: string[];
}[],
) => {
const callback = prefetchCallbacks[kind];
if (!callback) {
return;
}
const serverSettings = await cachedGetServerSettingsAsync(db);
const itemsWithDefaultOptions = items.map((item) => ({
...item,
options: reduceWidgetOptionsWithDefaultValues(kind, createSettings({ user: null, serverSettings }), item.options),
}));
await callback(queryClient, itemsWithDefaultOptions as never[]);
};

View File

@@ -141,7 +141,6 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
[classes.active ?? ""]: isActive,
})}
p="xs"
wrap="nowrap"
onClick={() => toggleExpandedRepository(repository.identifier)}
>
<MaskedOrNormalImage
@@ -153,7 +152,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
}}
/>
<Group gap={5} justify="space-between" style={{ flex: 1, minWidth: 0 }} wrap="nowrap">
<Group gap={5} justify="space-between" style={{ flex: 1 }}>
<Text size="xs">{repository.identifier}</Text>
<Tooltip label={repository.latestRelease ?? t("not-found")}>
@@ -163,7 +162,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
</Tooltip>
</Group>
<Group gap={5} wrap="nowrap">
<Group gap={5}>
<Text
size="xs"
c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"}

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { objectEntries } from "@homarr/common";
import type { SettingsContextProps } from "@homarr/settings";
import type { SettingsContextProps } from "@homarr/settings/creator";
import { createLanguageMapping } from "@homarr/translation";
import { widgetImports } from "..";

View File

@@ -1,16 +0,0 @@
diff --git a/src/index.ts b/src/index.ts
index c8f62a743b89040c1d10a8ae3795bec8afcbc134..7cd7c1e98c1c9516b40f678627f51fd1adfa18c7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -109,9 +109,9 @@ export function formatError(
(name) => name !== "stack" && name !== "message"
);
if (propNames.length > 0) {
- const props = {};
+ const props: Record<string, unknown> = {};
propNames.forEach((name) => {
- props[name] = err[name];
+ props[name] = (err as Record<string, object>)[name];
});
let propertiesString;

1428
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More