feat(widget): add prefetch for apps and bookmarks (#2895)
This commit is contained in:
@@ -4,15 +4,21 @@ import { TRPCError } from "@trpc/server";
|
|||||||
// Placed here because gridstack styles are used for board content
|
// Placed here because gridstack styles are used for board content
|
||||||
import "~/styles/gridstack.scss";
|
import "~/styles/gridstack.scss";
|
||||||
|
|
||||||
|
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getQueryClient } from "@homarr/api/server";
|
||||||
import { IntegrationProvider } from "@homarr/auth/client";
|
import { IntegrationProvider } from "@homarr/auth/client";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
||||||
import { isNullOrWhitespace } from "@homarr/common";
|
import { isNullOrWhitespace } from "@homarr/common";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
import { getI18n } from "@homarr/translation/server";
|
import { getI18n } from "@homarr/translation/server";
|
||||||
|
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
|
||||||
|
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { createBoardLayout } from "../_layout-creator";
|
import { createBoardLayout } from "../_layout-creator";
|
||||||
import type { Board } from "../_types";
|
import type { Board, Item } from "../_types";
|
||||||
import { DynamicClientBoard } from "./_dynamic-client";
|
import { DynamicClientBoard } from "./_dynamic-client";
|
||||||
import { BoardContentHeaderActions } from "./_header-actions";
|
import { BoardContentHeaderActions } from "./_header-actions";
|
||||||
|
|
||||||
@@ -31,14 +37,36 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
|
|||||||
getInitialBoardAsync: getInitialBoard,
|
getInitialBoardAsync: getInitialBoard,
|
||||||
}),
|
}),
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
page: async () => {
|
page: async ({ params }: { params: Promise<TParams> }) => {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const integrations = await getIntegrationsWithPermissionsAsync(session);
|
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 (
|
return (
|
||||||
<IntegrationProvider integrations={integrations}>
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
<DynamicClientBoard />
|
<IntegrationProvider integrations={integrations}>
|
||||||
</IntegrationProvider>
|
<DynamicClientBoard />
|
||||||
|
</IntegrationProvider>
|
||||||
|
</HydrationBoundary>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {
|
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import "@homarr/ui/styles.css";
|
|||||||
import "~/styles/scroll-area.scss";
|
import "~/styles/scroll-area.scss";
|
||||||
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import type { DayOfWeek } from "@mantine/dates";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -87,7 +88,15 @@ export default async function Layout(props: {
|
|||||||
},
|
},
|
||||||
(innerProps) => (
|
(innerProps) => (
|
||||||
<SettingsProvider
|
<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={{
|
serverSettings={{
|
||||||
board: {
|
board: {
|
||||||
homeBoardId: serverSettings.board.homeBoardId,
|
homeBoardId: serverSettings.board.homeBoardId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { WidgetKind } from "@homarr/definitions";
|
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 type { WidgetComponentProps } from "@homarr/widgets";
|
||||||
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";
|
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,11 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.1.2",
|
"@kubernetes/client-node": "^1.1.2",
|
||||||
|
"@tanstack/react-query": "^5.75.1",
|
||||||
"@trpc/client": "^11.1.2",
|
"@trpc/client": "^11.1.2",
|
||||||
"@trpc/react-query": "^11.1.2",
|
"@trpc/react-query": "^11.1.2",
|
||||||
"@trpc/server": "^11.1.2",
|
"@trpc/server": "^11.1.2",
|
||||||
|
"@trpc/tanstack-react-query": "^11.1.2",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import { headers } from "next/headers";
|
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 { auth } from "@homarr/auth/next";
|
||||||
|
|
||||||
|
import { makeQueryClient } from "./shared";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
|
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
|
||||||
* handling a tRPC call from a React Server Component.
|
* handling a tRPC call from a React Server Component.
|
||||||
@@ -19,3 +22,12 @@ const createContext = cache(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const api = createCaller(createContext);
|
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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a headers callback for a given source
|
* Creates a headers callback for a given source
|
||||||
* It will set the x-trpc-source header and cookies if needed
|
* It will set the x-trpc-source header and cookies if needed
|
||||||
@@ -51,3 +53,16 @@ export const trpcPath = "/api/trpc";
|
|||||||
export function getTrpcUrl() {
|
export function getTrpcUrl() {
|
||||||
return `${getBaseUrl()}${trpcPath}`;
|
return `${getBaseUrl()}${trpcPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const makeQueryClient = () => {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5 * 1000,
|
||||||
|
},
|
||||||
|
dehydrate: {
|
||||||
|
shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts",
|
||||||
|
"./creator": "./src/creator.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -2,30 +2,9 @@
|
|||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import type { DayOfWeek } from "@mantine/dates";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { PublicServerSettings, SettingsContextProps, UserSettings } from "./creator";
|
||||||
import type { User } from "@homarr/db/schema";
|
import { createSettings } from "./creator";
|
||||||
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"
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsContext = createContext<SettingsContextProps | null>(null);
|
const SettingsContext = createContext<SettingsContextProps | null>(null);
|
||||||
|
|
||||||
@@ -33,22 +12,9 @@ export const SettingsProvider = ({
|
|||||||
user,
|
user,
|
||||||
serverSettings,
|
serverSettings,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{ user: RouterOutputs["user"]["getById"] | null; serverSettings: PublicServerSettings }>) => {
|
}: PropsWithChildren<{ user: UserSettings | null; serverSettings: PublicServerSettings }>) => {
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider
|
<SettingsContext.Provider value={createSettings({ user, serverSettings })}>{children}</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
48
packages/settings/src/creator.ts
Normal file
48
packages/settings/src/creator.ts
Normal 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,
|
||||||
|
});
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./errors": "./src/errors/component.tsx",
|
"./errors": "./src/errors/component.tsx",
|
||||||
"./modals": "./src/modals/index.ts"
|
"./modals": "./src/modals/index.ts",
|
||||||
|
"./prefetch": "./src/prefetch.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
|
|||||||
23
packages/widgets/src/app/prefetch.ts
Normal file
23
packages/widgets/src/app/prefetch.ts
Normal 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;
|
||||||
30
packages/widgets/src/bookmarks/prefetch.ts
Normal file
30
packages/widgets/src/bookmarks/prefetch.ts
Normal 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;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { LoaderComponent } from "next/dynamic";
|
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 { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
|
||||||
|
|
||||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||||
import type { ServerSettings } from "@homarr/server-settings";
|
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 { stringOrTranslation } from "@homarr/translation";
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
@@ -21,6 +22,15 @@ const createWithDynamicImport =
|
|||||||
componentLoader,
|
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>(
|
export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition extends WidgetDefinition>(
|
||||||
kind: TKind,
|
kind: TKind,
|
||||||
definition: TDefinition,
|
definition: TDefinition,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Center, Loader as UiLoader } from "@mantine/core";
|
|||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
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 app from "./app";
|
||||||
import * as bookmarks from "./bookmarks";
|
import * as bookmarks from "./bookmarks";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { objectEntries } from "@homarr/common";
|
|||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { zodResolver } from "@homarr/form";
|
import { zodResolver } from "@homarr/form";
|
||||||
import { createModal, useModalAction } from "@homarr/modals";
|
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 { useI18n } from "@homarr/translation/client";
|
||||||
import { zodErrorMap } from "@homarr/validation/form/i18n";
|
import { zodErrorMap } from "@homarr/validation/form/i18n";
|
||||||
|
|
||||||
|
|||||||
45
packages/widgets/src/prefetch.ts
Normal file
45
packages/widgets/src/prefetch.ts
Normal 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[]);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import type { SettingsContextProps } from "@homarr/settings";
|
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||||
import { createLanguageMapping } from "@homarr/translation";
|
import { createLanguageMapping } from "@homarr/translation";
|
||||||
|
|
||||||
import { widgetImports } from "..";
|
import { widgetImports } from "..";
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -576,6 +576,9 @@ importers:
|
|||||||
'@kubernetes/client-node':
|
'@kubernetes/client-node':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
|
'@tanstack/react-query':
|
||||||
|
specifier: ^5.75.1
|
||||||
|
version: 5.75.1(react@19.1.0)
|
||||||
'@trpc/client':
|
'@trpc/client':
|
||||||
specifier: ^11.1.2
|
specifier: ^11.1.2
|
||||||
version: 11.1.2(@trpc/server@11.1.2(typescript@5.8.3))(typescript@5.8.3)
|
version: 11.1.2(@trpc/server@11.1.2(typescript@5.8.3))(typescript@5.8.3)
|
||||||
@@ -585,6 +588,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.1.2
|
specifier: ^11.1.2
|
||||||
version: 11.1.2(typescript@5.8.3)
|
version: 11.1.2(typescript@5.8.3)
|
||||||
|
'@trpc/tanstack-react-query':
|
||||||
|
specifier: ^11.1.2
|
||||||
|
version: 11.1.2(@tanstack/react-query@5.75.1(react@19.1.0))(@trpc/client@11.1.2(@trpc/server@11.1.2(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.1.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||||
lodash.clonedeep:
|
lodash.clonedeep:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
@@ -2046,6 +2052,9 @@ importers:
|
|||||||
'@homarr/integrations':
|
'@homarr/integrations':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../integrations
|
version: link:../integrations
|
||||||
|
'@homarr/log':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../log
|
||||||
'@homarr/modals':
|
'@homarr/modals':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../modals
|
version: link:../modals
|
||||||
@@ -4479,6 +4488,16 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.7.2'
|
typescript: '>=5.7.2'
|
||||||
|
|
||||||
|
'@trpc/tanstack-react-query@11.1.2':
|
||||||
|
resolution: {integrity: sha512-c+NupnmIQmwgwYVTaHgDg59BZBtJ6KxqB0cxF9FBhKHPY6PlwGLdU8+mo25C5VIpofZYEII4H7NKE4yKGFSe3w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tanstack/react-query': ^5.67.1
|
||||||
|
'@trpc/client': 11.1.2
|
||||||
|
'@trpc/server': 11.1.2
|
||||||
|
react: '>=18.2.0'
|
||||||
|
react-dom: '>=18.2.0'
|
||||||
|
typescript: '>=5.7.2'
|
||||||
|
|
||||||
'@tsconfig/node10@1.0.11':
|
'@tsconfig/node10@1.0.11':
|
||||||
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
|
||||||
|
|
||||||
@@ -13072,6 +13091,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
||||||
|
'@trpc/tanstack-react-query@11.1.2(@tanstack/react-query@5.75.1(react@19.1.0))(@trpc/client@11.1.2(@trpc/server@11.1.2(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.1.2(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/react-query': 5.75.1(react@19.1.0)
|
||||||
|
'@trpc/client': 11.1.2(@trpc/server@11.1.2(typescript@5.8.3))(typescript@5.8.3)
|
||||||
|
'@trpc/server': 11.1.2(typescript@5.8.3)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
'@tsconfig/node10@1.0.11': {}
|
'@tsconfig/node10@1.0.11': {}
|
||||||
|
|
||||||
'@tsconfig/node12@1.0.11': {}
|
'@tsconfig/node12@1.0.11': {}
|
||||||
|
|||||||
Reference in New Issue
Block a user