add server side fetching

This commit is contained in:
miloschwartz
2026-06-30 21:15:03 -04:00
parent f0efa4203b
commit fed4ec42c4
8 changed files with 411 additions and 313 deletions

View File

@@ -0,0 +1,9 @@
import { Loader2 } from "lucide-react";
export default function OrgPageLoading() {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
);
}

View File

@@ -2,8 +2,8 @@ import { Layout } from "@app/components/Layout";
import ResourceLauncher from "@app/components/resource-launcher/ResourceLauncher";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { fetchLauncherPageData } from "@app/lib/launcherServerData";
import { verifySession } from "@app/lib/auth/verifySession";
import { pullEnv } from "@app/lib/pullEnv";
import UserProvider from "@app/providers/UserProvider";
import { ListUserOrgsResponse } from "@server/routers/org";
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
@@ -13,8 +13,11 @@ import { cache } from "react";
type OrgPageProps = {
params: Promise<{ orgId: string }>;
searchParams: Promise<Record<string, string>>;
};
export const dynamic = "force-dynamic";
export default async function OrgPage(props: OrgPageProps) {
const params = await props.params;
const orgId = params.orgId;
@@ -55,6 +58,15 @@ export default async function OrgPage(props: OrgPageProps) {
const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner);
const searchParams = new URLSearchParams(await props.searchParams);
const launcherData = overview
? await fetchLauncherPageData(
orgId,
searchParams,
await authCookieHeader()
)
: null;
return (
<UserProvider user={user}>
<Layout
@@ -65,8 +77,18 @@ export default async function OrgPage(props: OrgPageProps) {
launcherMode
showViewAsAdmin={isAdminOrOwner}
>
{overview ? (
<ResourceLauncher orgId={orgId} isAdmin={isAdminOrOwner} />
{overview && launcherData ? (
<ResourceLauncher
orgId={orgId}
isAdmin={isAdminOrOwner}
views={launcherData.views}
activeViewId={launcherData.activeViewId}
config={launcherData.config}
savedConfig={launcherData.savedConfig}
groups={launcherData.groups}
groupsPagination={launcherData.groupsPagination}
resourcesByGroupKey={launcherData.resourcesByGroupKey}
/>
) : null}
</Layout>
</UserProvider>

View File

@@ -1,159 +1,80 @@
"use client";
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
import { readLauncherGroupOpen } from "@app/lib/launcherLocalStorage";
import type { LauncherGroupResources } from "@app/lib/launcherServerData";
import { launcherQueries } from "@app/lib/queries";
import type { LauncherViewConfig } from "@server/routers/launcher/types";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import type {
LauncherGroup,
LauncherViewConfig
} from "@server/routers/launcher/types";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef } from "react";
import { LauncherGroupSection } from "./LauncherGroupSection";
type LauncherGroupListProps = {
orgId: string;
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
searchQuery: string;
};
function buildResourceFilters(
config: LauncherViewConfig,
searchQuery: string,
groupKey: string
) {
return {
query: searchQuery,
groupBy: config.groupBy,
groupKey,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order,
pageSize: 20
initialGroups: LauncherGroup[];
groupsPagination: {
total: number;
page: number;
pageSize: number;
};
}
resourcesByGroupKey: Record<string, LauncherGroupResources>;
};
export function LauncherGroupList({
orgId,
activeViewId,
config,
searchQuery
initialGroups,
groupsPagination,
resourcesByGroupKey
}: LauncherGroupListProps) {
const queryClient = useQueryClient();
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const [isPrefetching, setIsPrefetching] = useState(false);
const prefetchBatchKeyRef = useRef<string | null>(null);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
const groupFilters = useMemo(
() => ({
query: config.query,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order,
pageSize: 20
}),
[
config.groupBy,
config.labelIds,
config.order,
config.query,
config.siteIds,
config.sortBy
]
);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
...launcherQueries.groups(orgId, {
query: searchQuery,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order,
pageSize: 20
})
...launcherQueries.groups(orgId, groupFilters),
initialData: {
pages: [
{
groups: initialGroups,
pagination: groupsPagination
}
],
pageParams: [1]
},
refetchOnMount: false
});
const groups = data?.pages.flatMap((page) => page.groups) ?? [];
const batchKey = useMemo(
() =>
JSON.stringify({
activeViewId,
searchQuery,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sortBy: config.sortBy,
order: config.order
}),
[
activeViewId,
config.groupBy,
config.labelIds,
config.order,
config.siteIds,
config.sortBy,
searchQuery
]
);
const openGroupKeys = useMemo(
() =>
groups
.filter((group) =>
readLauncherGroupOpen(
orgId,
activeViewId,
config.groupBy,
group.groupKey,
true
)
)
.map((group) => group.groupKey),
[activeViewId, config.groupBy, groups, orgId]
);
useEffect(() => {
if (isLoading) {
return;
}
if (openGroupKeys.length === 0) {
prefetchBatchKeyRef.current = batchKey;
setIsPrefetching(false);
return;
}
if (prefetchBatchKeyRef.current === batchKey) {
return;
}
let cancelled = false;
setIsPrefetching(true);
void Promise.all(
openGroupKeys.map((groupKey) =>
queryClient.prefetchInfiniteQuery(
launcherQueries.resources(
orgId,
buildResourceFilters(config, searchQuery, groupKey)
)
)
)
).finally(() => {
if (!cancelled) {
prefetchBatchKeyRef.current = batchKey;
setIsPrefetching(false);
}
});
return () => {
cancelled = true;
};
}, [
batchKey,
config,
isLoading,
openGroupKeys,
orgId,
queryClient,
searchQuery
]);
const isBatchPending = prefetchBatchKeyRef.current !== batchKey;
const isBodyLoading =
isLoading ||
(isBatchPending &&
openGroupKeys.length > 0 &&
(isPrefetching || !isLoading));
useEffect(() => {
const node = loadMoreRef.current;
if (!node || !hasNextPage || isBodyLoading) {
if (!node || !hasNextPage) {
return;
}
@@ -168,28 +89,25 @@ export function LauncherGroupList({
observer.observe(node);
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isBodyLoading, isFetchingNextPage]);
if (isBodyLoading) {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
);
}
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
return (
<div className="flex flex-col gap-2.5">
{groups.map((group) => (
<LauncherGroupSection
key={group.groupKey}
orgId={orgId}
activeViewId={activeViewId}
group={group}
config={config}
searchQuery={searchQuery}
/>
))}
{groups.map((group) => {
const groupResources = resourcesByGroupKey[group.groupKey];
return (
<LauncherGroupSection
key={group.groupKey}
orgId={orgId}
activeViewId={activeViewId}
group={group}
config={config}
initialResources={groupResources?.resources}
initialResourcesPagination={groupResources?.pagination}
/>
);
})}
<div ref={loadMoreRef} className="h-4" />
{isFetchingNextPage ? (
<div className="flex justify-center py-2">

View File

@@ -13,6 +13,7 @@ import {
import { launcherQueries } from "@app/lib/queries";
import type {
LauncherGroup,
LauncherResource,
LauncherViewConfig
} from "@server/routers/launcher/types";
import { useInfiniteQuery } from "@tanstack/react-query";
@@ -28,7 +29,12 @@ type LauncherGroupSectionProps = {
activeViewId: LauncherActiveViewId;
group: LauncherGroup;
config: LauncherViewConfig;
searchQuery: string;
initialResources?: LauncherResource[];
initialResourcesPagination?: {
total: number;
page: number;
pageSize: number;
};
defaultOpen?: boolean;
};
@@ -37,7 +43,8 @@ export function LauncherGroupSection({
activeViewId,
group,
config,
searchQuery,
initialResources,
initialResourcesPagination,
defaultOpen = true
}: LauncherGroupSectionProps) {
const t = useTranslations();
@@ -75,10 +82,12 @@ export function LauncherGroupSection({
);
};
const hasInitialResources = initialResources !== undefined;
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
useInfiniteQuery({
...launcherQueries.resources(orgId, {
query: searchQuery,
query: config.query,
groupBy: config.groupBy,
groupKey: group.groupKey,
siteIds: config.siteIds,
@@ -87,14 +96,33 @@ export function LauncherGroupSection({
order: config.order,
pageSize: 20
}),
enabled: isOpen
enabled: isOpen,
refetchOnMount: false,
...(hasInitialResources
? {
initialData: {
pages: [
{
resources: initialResources,
pagination: initialResourcesPagination ?? {
total: initialResources.length,
page: 1,
pageSize: 20
}
}
],
pageParams: [1]
}
}
: {})
});
const resources = data?.pages.flatMap((page) => page.resources) ?? [];
const showInitialLoader = isLoading && resources.length === 0;
useEffect(() => {
const node = loadMoreRef.current;
if (!node || !hasNextPage) {
if (!node || !hasNextPage || !isOpen) {
return;
}
@@ -109,7 +137,7 @@ export function LauncherGroupSection({
observer.observe(node);
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
}, [fetchNextPage, hasNextPage, isFetchingNextPage, isOpen]);
const groupTitle =
group.groupKey === "unlabeled"
@@ -129,7 +157,7 @@ export function LauncherGroupSection({
/>
<CollapsibleContent className="w-full">
{isLoading ? (
{showInitialLoader ? (
<div className="flex items-center justify-center py-10 text-muted-foreground">
<Loader2 className="size-5 animate-spin" />
</div>

View File

@@ -14,30 +14,31 @@ import { CheckboxWithLabel } from "@app/components/ui/checkbox";
import { Input } from "@app/components/ui/input";
import { Label } from "@app/components/ui/label";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useNavigationContext } from "@app/hooks/useNavigationContext";
import {
readLauncherLastView,
writeLauncherLastView,
type LauncherActiveViewId
} from "@app/lib/launcherLocalStorage";
import type { LauncherGroupResources } from "@app/lib/launcherServerData";
import {
buildLauncherPath,
getLauncherUrlBaseConfig,
isLauncherConfigEqual,
resolveLauncherStateFromUrl,
parseLauncherUrlState,
serializeLauncherUrlState
} from "@app/lib/launcherUrlState";
import { launcherQueries } from "@app/lib/queries";
import { useToast } from "@app/hooks/useToast";
import { useEnvContext } from "@app/hooks/useEnvContext";
import {
defaultLauncherViewConfig,
type LauncherViewConfig,
type LauncherViewRecord
import type {
LauncherGroup,
LauncherViewConfig,
LauncherViewRecord
} from "@server/routers/launcher/types";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { Search } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import type { Selectedsite } from "@app/components/site-selector";
@@ -52,33 +53,39 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
type ResourceLauncherProps = {
orgId: string;
isAdmin: boolean;
views: LauncherViewRecord[];
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
savedConfig: LauncherViewConfig;
groups: LauncherGroup[];
groupsPagination: {
total: number;
page: number;
pageSize: number;
};
resourcesByGroupKey: Record<string, LauncherGroupResources>;
};
export default function ResourceLauncher({
orgId,
isAdmin
isAdmin,
views,
activeViewId,
config,
savedConfig,
groups,
groupsPagination,
resourcesByGroupKey
}: ResourceLauncherProps) {
const t = useTranslations();
const { toast } = useToast();
const { env } = useEnvContext();
const queryClient = useQueryClient();
const api = createApiClient({ env });
const router = useRouter();
const searchParams = useSearchParams();
const [activeViewId, setActiveViewId] =
useState<LauncherActiveViewId>("default");
const { navigate, isNavigating, searchParams } = useNavigationContext();
const hasRestoredLastView = useRef(false);
const isApplyingUrlRef = useRef(false);
const [config, setConfig] = useState<LauncherViewConfig>(
defaultLauncherViewConfig
);
const [savedConfig, setSavedConfig] = useState<LauncherViewConfig>(
defaultLauncherViewConfig
);
const [searchInput, setSearchInput] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [searchInput, setSearchInput] = useState(config.query);
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [newViewName, setNewViewName] = useState("");
const [saveOrgWide, setSaveOrgWide] = useState(false);
@@ -90,68 +97,66 @@ export default function ResourceLauncher({
const activeViewIdRef = useRef(activeViewId);
activeViewIdRef.current = activeViewId;
const { data: views = [], isLoading: viewsLoading } = useQuery(
launcherQueries.views(orgId)
);
useEffect(() => {
setSearchInput(config.query);
}, [config.query]);
const syncUrl = useCallback(
useEffect(() => {
if (hasRestoredLastView.current) {
return;
}
hasRestoredLastView.current = true;
const parsed = parseLauncherUrlState(searchParams);
if (parsed.hasAnyLauncherParams) {
return;
}
const lastView = readLauncherLastView(orgId);
if (lastView === null || lastView === activeViewId) {
return;
}
const isValid =
lastView === "default" ||
views.some((view) => view.viewId === lastView);
if (!isValid) {
return;
}
const baseConfig = getLauncherUrlBaseConfig(lastView, views);
const params = serializeLauncherUrlState({
viewId: lastView,
config: baseConfig
});
navigate({ searchParams: params, replace: true });
}, [activeViewId, navigate, orgId, searchParams, views]);
const navigateToConfig = useCallback(
(viewId: LauncherActiveViewId, nextConfig: LauncherViewConfig) => {
if (isApplyingUrlRef.current) {
return;
}
const params = serializeLauncherUrlState({
viewId,
config: nextConfig
});
const path = buildLauncherPath(orgId, params);
router.replace(path, { scroll: false });
navigate({ searchParams: params });
},
[orgId, router]
[navigate]
);
const debouncedSyncSearch = useDebouncedCallback(
const debouncedNavigateSearch = useDebouncedCallback(
(viewId: LauncherActiveViewId, query: string) => {
const nextConfig = { ...configRef.current, query };
setSearchQuery(query);
syncUrl(viewId, nextConfig);
navigateToConfig(viewId, { ...configRef.current, query });
},
300
);
useEffect(() => {
if (viewsLoading) {
return;
}
let fallbackViewId: LauncherActiveViewId | null = null;
if (!hasRestoredLastView.current) {
hasRestoredLastView.current = true;
fallbackViewId = readLauncherLastView(orgId);
}
isApplyingUrlRef.current = true;
const resolved = resolveLauncherStateFromUrl(
new URLSearchParams(searchParams),
views,
fallbackViewId
);
setActiveViewId(resolved.activeViewId);
setConfig(resolved.config);
setSavedConfig(resolved.savedConfig);
setSearchInput(resolved.config.query);
setSearchQuery(resolved.config.query);
isApplyingUrlRef.current = false;
}, [orgId, searchParams, views, viewsLoading]);
const selectView = useCallback(
(viewId: LauncherActiveViewId) => {
writeLauncherLastView(orgId, viewId);
const baseConfig = getLauncherUrlBaseConfig(viewId, views);
syncUrl(viewId, baseConfig);
navigateToConfig(viewId, baseConfig);
},
[orgId, syncUrl, views]
[navigateToConfig, orgId, views]
);
const activeSavedView = useMemo(
@@ -186,12 +191,6 @@ export default function ResourceLauncher({
[config.labelIds]
);
const invalidateLauncher = () => {
void queryClient.invalidateQueries({
queryKey: ["ORG", orgId, "LAUNCHER"]
});
};
const createViewMutation = useMutation({
mutationFn: async (payload: {
name: string;
@@ -202,23 +201,13 @@ export default function ResourceLauncher({
return res.data.data as LauncherViewRecord;
},
onSuccess: (view) => {
invalidateLauncher();
writeLauncherLastView(orgId, view.viewId);
isApplyingUrlRef.current = true;
setActiveViewId(view.viewId);
setConfig(view.config);
setSavedConfig(view.config);
setSearchInput(view.config.query);
setSearchQuery(view.config.query);
isApplyingUrlRef.current = false;
const params = serializeLauncherUrlState({
viewId: view.viewId,
config: view.config
});
router.replace(buildLauncherPath(orgId, params), { scroll: false });
navigate({ searchParams: params, replace: true });
router.refresh();
setSaveDialogOpen(false);
setNewViewName("");
toast({
@@ -253,22 +242,12 @@ export default function ResourceLauncher({
return res.data.data as LauncherViewRecord;
},
onSuccess: (view) => {
invalidateLauncher();
isApplyingUrlRef.current = true;
setActiveViewId(view.viewId);
setConfig(view.config);
setSavedConfig(view.config);
setSearchInput(view.config.query);
setSearchQuery(view.config.query);
isApplyingUrlRef.current = false;
const params = serializeLauncherUrlState({
viewId: view.viewId,
config: view.config
});
router.replace(buildLauncherPath(orgId, params), { scroll: false });
navigate({ searchParams: params, replace: true });
router.refresh();
toast({
title: t("resourceLauncherViewSaved"),
description: t("resourceLauncherViewSavedDescription")
@@ -291,8 +270,13 @@ export default function ResourceLauncher({
await api.delete(`/org/${orgId}/launcher/views/${viewId}`);
},
onSuccess: () => {
invalidateLauncher();
selectView("default");
writeLauncherLastView(orgId, "default");
const params = serializeLauncherUrlState({
viewId: "default",
config: getLauncherUrlBaseConfig("default", views)
});
navigate({ searchParams: params, replace: true });
router.refresh();
toast({
title: t("resourceLauncherViewDeleted"),
description: t("resourceLauncherViewDeletedDescription")
@@ -317,9 +301,9 @@ export default function ResourceLauncher({
...patch,
query: searchInputRef.current
};
syncUrl(activeViewIdRef.current, nextConfig);
navigateToConfig(activeViewIdRef.current, nextConfig);
},
[syncUrl]
[navigateToConfig]
);
const handleSaveToCurrent = () => {
@@ -370,7 +354,7 @@ export default function ResourceLauncher({
};
return (
<div className="flex flex-col">
<div className="flex flex-col" aria-busy={isNavigating}>
<SettingsSectionTitle
title={t("resourceLauncherTitle")}
description={t("resourceLauncherDescription")}
@@ -386,7 +370,7 @@ export default function ResourceLauncher({
onChange={(event) => {
const value = event.target.value;
setSearchInput(value);
debouncedSyncSearch(
debouncedNavigateSearch(
activeViewIdRef.current,
value
);
@@ -397,16 +381,14 @@ export default function ResourceLauncher({
className="pl-8"
/>
</div>
{!viewsLoading ? (
<LauncherViewTabs
activeViewId={activeViewId}
savedViews={views.map((view) => ({
viewId: view.viewId,
name: view.name
}))}
onSelectView={selectView}
/>
) : null}
<LauncherViewTabs
activeViewId={activeViewId}
savedViews={views.map((view) => ({
viewId: view.viewId,
name: view.name
}))}
onSelectView={selectView}
/>
</div>
<div className="flex items-center gap-2 shrink-0 justify-end">
<LauncherSaveViewMenu
@@ -462,8 +444,10 @@ export default function ResourceLauncher({
<LauncherGroupList
orgId={orgId}
activeViewId={activeViewId}
config={{ ...config, query: searchQuery }}
searchQuery={searchQuery}
config={config}
initialGroups={groups}
groupsPagination={groupsPagination}
resourcesByGroupKey={resourcesByGroupKey}
/>
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>

View File

@@ -0,0 +1,43 @@
import type { LauncherListQuery } from "@server/routers/launcher/types";
export type LauncherQueryFilters = {
query?: string;
groupBy?: LauncherListQuery["groupBy"];
groupKey?: string;
siteIds?: number[];
labelIds?: number[];
sort_by?: LauncherListQuery["sort_by"];
order?: LauncherListQuery["order"];
pageSize?: number;
};
export function buildLauncherSearchParams(
filters: LauncherQueryFilters,
page: number
) {
const sp = new URLSearchParams();
sp.set("page", String(page));
sp.set("pageSize", String(filters.pageSize ?? 20));
if (filters.query) {
sp.set("query", filters.query);
}
if (filters.groupBy) {
sp.set("groupBy", filters.groupBy);
}
if (filters.groupKey) {
sp.set("groupKey", filters.groupKey);
}
if (filters.siteIds?.length) {
sp.set("siteIds", filters.siteIds.join(","));
}
if (filters.labelIds?.length) {
sp.set("labelIds", filters.labelIds.join(","));
}
if (filters.sort_by) {
sp.set("sort_by", filters.sort_by);
}
if (filters.order) {
sp.set("order", filters.order);
}
return sp;
}

View File

@@ -0,0 +1,128 @@
import { internal } from "@app/lib/api";
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
import { resolveLauncherStateFromUrl } from "@app/lib/launcherUrlState";
import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
import type {
LauncherGroup,
LauncherResource,
LauncherViewConfig,
LauncherViewRecord,
ListLauncherGroupsResponse,
ListLauncherResourcesResponse,
ListLauncherViewsResponse
} from "@server/routers/launcher/types";
import { AxiosResponse } from "axios";
export type LauncherGroupResources = {
resources: LauncherResource[];
pagination: {
total: number;
page: number;
pageSize: number;
};
};
export type LauncherPageData = {
views: LauncherViewRecord[];
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
savedConfig: LauncherViewConfig;
groups: LauncherGroup[];
groupsPagination: {
total: number;
page: number;
pageSize: number;
};
resourcesByGroupKey: Record<string, LauncherGroupResources>;
};
const emptyResources: LauncherGroupResources = {
resources: [],
pagination: { total: 0, page: 1, pageSize: 20 }
};
export async function fetchLauncherPageData(
orgId: string,
searchParams: URLSearchParams,
cookieHeader: Awaited<
ReturnType<typeof import("@app/lib/api/cookies").authCookieHeader>
>
): Promise<LauncherPageData> {
let views: LauncherViewRecord[] = [];
try {
const viewsRes = await internal.get<
AxiosResponse<ListLauncherViewsResponse>
>(`/org/${orgId}/launcher/views`, cookieHeader);
views = viewsRes.data.data.views;
} catch (e) {}
const { activeViewId, config, savedConfig } = resolveLauncherStateFromUrl(
searchParams,
views,
null
);
const groupFilters = {
query: config.query,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order,
pageSize: 20
};
let groups: LauncherGroup[] = [];
let groupsPagination: LauncherPageData["groupsPagination"] = {
total: 0,
page: 1,
pageSize: 20
};
try {
const sp = buildLauncherSearchParams(groupFilters, 1);
const groupsRes = await internal.get<
AxiosResponse<ListLauncherGroupsResponse>
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, cookieHeader);
groups = groupsRes.data.data.groups;
groupsPagination = groupsRes.data.data.pagination;
} catch (e) {}
const resourcesByGroupKey: Record<string, LauncherGroupResources> = {};
await Promise.all(
groups.map(async (group) => {
try {
const sp = buildLauncherSearchParams(
{
...groupFilters,
groupKey: group.groupKey
},
1
);
const res = await internal.get<
AxiosResponse<ListLauncherResourcesResponse>
>(
`/org/${orgId}/launcher/resources?${sp.toString()}`,
cookieHeader
);
resourcesByGroupKey[group.groupKey] = {
resources: res.data.data.resources,
pagination: res.data.data.pagination
};
} catch (e) {
resourcesByGroupKey[group.groupKey] = emptyResources;
}
})
);
return {
views,
activeViewId,
config,
savedConfig,
groups,
groupsPagination,
resourcesByGroupKey
};
}

View File

@@ -53,6 +53,11 @@ import type {
LauncherListQuery,
LauncherViewConfig
} from "@server/routers/launcher/types";
import type { LauncherQueryFilters } from "@app/lib/launcherSearchParams";
import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
export type { LauncherQueryFilters } from "@app/lib/launcherSearchParams";
export { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
export type ProductUpdate = {
link: string | null;
@@ -1174,45 +1179,6 @@ export const domainQueries = {
})
};
export type LauncherQueryFilters = {
query?: string;
groupBy?: LauncherListQuery["groupBy"];
groupKey?: string;
siteIds?: number[];
labelIds?: number[];
sort_by?: LauncherListQuery["sort_by"];
order?: LauncherListQuery["order"];
pageSize?: number;
};
function launcherSearchParams(filters: LauncherQueryFilters, page: number) {
const sp = new URLSearchParams();
sp.set("page", String(page));
sp.set("pageSize", String(filters.pageSize ?? 20));
if (filters.query) {
sp.set("query", filters.query);
}
if (filters.groupBy) {
sp.set("groupBy", filters.groupBy);
}
if (filters.groupKey) {
sp.set("groupKey", filters.groupKey);
}
if (filters.siteIds?.length) {
sp.set("siteIds", filters.siteIds.join(","));
}
if (filters.labelIds?.length) {
sp.set("labelIds", filters.labelIds.join(","));
}
if (filters.sort_by) {
sp.set("sort_by", filters.sort_by);
}
if (filters.order) {
sp.set("order", filters.order);
}
return sp;
}
export const launcherQueries = {
views: (orgId: string) =>
queryOptions({
@@ -1228,7 +1194,7 @@ export const launcherQueries = {
infiniteQueryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
queryFn: async ({ pageParam = 1, signal, meta }) => {
const sp = launcherSearchParams(filters, pageParam);
const sp = buildLauncherSearchParams(filters, pageParam);
const res = await meta!.api.get<
AxiosResponse<ListLauncherGroupsResponse>
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, { signal });
@@ -1249,7 +1215,7 @@ export const launcherQueries = {
infiniteQueryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "RESOURCES", filters] as const,
queryFn: async ({ pageParam = 1, signal, meta }) => {
const sp = launcherSearchParams(filters, pageParam);
const sp = buildLauncherSearchParams(filters, pageParam);
const res = await meta!.api.get<
AxiosResponse<ListLauncherResourcesResponse>
>(`/org/${orgId}/launcher/resources?${sp.toString()}`, {