mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 04:10:13 +00:00
add server side fetching
This commit is contained in:
9
src/app/[orgId]/loading.tsx
Normal file
9
src/app/[orgId]/loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import { Layout } from "@app/components/Layout";
|
|||||||
import ResourceLauncher from "@app/components/resource-launcher/ResourceLauncher";
|
import ResourceLauncher from "@app/components/resource-launcher/ResourceLauncher";
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import { fetchLauncherPageData } from "@app/lib/launcherServerData";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { ListUserOrgsResponse } from "@server/routers/org";
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
|
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
|
||||||
@@ -13,8 +13,11 @@ import { cache } from "react";
|
|||||||
|
|
||||||
type OrgPageProps = {
|
type OrgPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function OrgPage(props: OrgPageProps) {
|
export default async function OrgPage(props: OrgPageProps) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const orgId = params.orgId;
|
const orgId = params.orgId;
|
||||||
@@ -55,6 +58,15 @@ export default async function OrgPage(props: OrgPageProps) {
|
|||||||
|
|
||||||
const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner);
|
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 (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout
|
<Layout
|
||||||
@@ -65,8 +77,18 @@ export default async function OrgPage(props: OrgPageProps) {
|
|||||||
launcherMode
|
launcherMode
|
||||||
showViewAsAdmin={isAdminOrOwner}
|
showViewAsAdmin={isAdminOrOwner}
|
||||||
>
|
>
|
||||||
{overview ? (
|
{overview && launcherData ? (
|
||||||
<ResourceLauncher orgId={orgId} isAdmin={isAdminOrOwner} />
|
<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}
|
) : null}
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
|
|||||||
@@ -1,159 +1,80 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
|
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 { launcherQueries } from "@app/lib/queries";
|
||||||
import type { LauncherViewConfig } from "@server/routers/launcher/types";
|
import type {
|
||||||
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
LauncherGroup,
|
||||||
|
LauncherViewConfig
|
||||||
|
} from "@server/routers/launcher/types";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { LauncherGroupSection } from "./LauncherGroupSection";
|
import { LauncherGroupSection } from "./LauncherGroupSection";
|
||||||
|
|
||||||
type LauncherGroupListProps = {
|
type LauncherGroupListProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
activeViewId: LauncherActiveViewId;
|
activeViewId: LauncherActiveViewId;
|
||||||
config: LauncherViewConfig;
|
config: LauncherViewConfig;
|
||||||
searchQuery: string;
|
initialGroups: LauncherGroup[];
|
||||||
};
|
groupsPagination: {
|
||||||
|
total: number;
|
||||||
function buildResourceFilters(
|
page: number;
|
||||||
config: LauncherViewConfig,
|
pageSize: number;
|
||||||
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
|
|
||||||
};
|
};
|
||||||
}
|
resourcesByGroupKey: Record<string, LauncherGroupResources>;
|
||||||
|
};
|
||||||
|
|
||||||
export function LauncherGroupList({
|
export function LauncherGroupList({
|
||||||
orgId,
|
orgId,
|
||||||
activeViewId,
|
activeViewId,
|
||||||
config,
|
config,
|
||||||
searchQuery
|
initialGroups,
|
||||||
|
groupsPagination,
|
||||||
|
resourcesByGroupKey
|
||||||
}: LauncherGroupListProps) {
|
}: LauncherGroupListProps) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
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({
|
useInfiniteQuery({
|
||||||
...launcherQueries.groups(orgId, {
|
...launcherQueries.groups(orgId, groupFilters),
|
||||||
query: searchQuery,
|
initialData: {
|
||||||
groupBy: config.groupBy,
|
pages: [
|
||||||
siteIds: config.siteIds,
|
{
|
||||||
labelIds: config.labelIds,
|
groups: initialGroups,
|
||||||
sort_by: config.sortBy,
|
pagination: groupsPagination
|
||||||
order: config.order,
|
}
|
||||||
pageSize: 20
|
],
|
||||||
})
|
pageParams: [1]
|
||||||
|
},
|
||||||
|
refetchOnMount: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const groups = data?.pages.flatMap((page) => page.groups) ?? [];
|
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(() => {
|
useEffect(() => {
|
||||||
const node = loadMoreRef.current;
|
const node = loadMoreRef.current;
|
||||||
if (!node || !hasNextPage || isBodyLoading) {
|
if (!node || !hasNextPage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,28 +89,25 @@ export function LauncherGroupList({
|
|||||||
|
|
||||||
observer.observe(node);
|
observer.observe(node);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [fetchNextPage, hasNextPage, isBodyLoading, isFetchingNextPage]);
|
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||||
|
|
||||||
if (isBodyLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center py-16">
|
|
||||||
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => {
|
||||||
<LauncherGroupSection
|
const groupResources = resourcesByGroupKey[group.groupKey];
|
||||||
key={group.groupKey}
|
|
||||||
orgId={orgId}
|
return (
|
||||||
activeViewId={activeViewId}
|
<LauncherGroupSection
|
||||||
group={group}
|
key={group.groupKey}
|
||||||
config={config}
|
orgId={orgId}
|
||||||
searchQuery={searchQuery}
|
activeViewId={activeViewId}
|
||||||
/>
|
group={group}
|
||||||
))}
|
config={config}
|
||||||
|
initialResources={groupResources?.resources}
|
||||||
|
initialResourcesPagination={groupResources?.pagination}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div ref={loadMoreRef} className="h-4" />
|
<div ref={loadMoreRef} className="h-4" />
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<div className="flex justify-center py-2">
|
<div className="flex justify-center py-2">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { launcherQueries } from "@app/lib/queries";
|
import { launcherQueries } from "@app/lib/queries";
|
||||||
import type {
|
import type {
|
||||||
LauncherGroup,
|
LauncherGroup,
|
||||||
|
LauncherResource,
|
||||||
LauncherViewConfig
|
LauncherViewConfig
|
||||||
} from "@server/routers/launcher/types";
|
} from "@server/routers/launcher/types";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
@@ -28,7 +29,12 @@ type LauncherGroupSectionProps = {
|
|||||||
activeViewId: LauncherActiveViewId;
|
activeViewId: LauncherActiveViewId;
|
||||||
group: LauncherGroup;
|
group: LauncherGroup;
|
||||||
config: LauncherViewConfig;
|
config: LauncherViewConfig;
|
||||||
searchQuery: string;
|
initialResources?: LauncherResource[];
|
||||||
|
initialResourcesPagination?: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +43,8 @@ export function LauncherGroupSection({
|
|||||||
activeViewId,
|
activeViewId,
|
||||||
group,
|
group,
|
||||||
config,
|
config,
|
||||||
searchQuery,
|
initialResources,
|
||||||
|
initialResourcesPagination,
|
||||||
defaultOpen = true
|
defaultOpen = true
|
||||||
}: LauncherGroupSectionProps) {
|
}: LauncherGroupSectionProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -75,10 +82,12 @@ export function LauncherGroupSection({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasInitialResources = initialResources !== undefined;
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||||
useInfiniteQuery({
|
useInfiniteQuery({
|
||||||
...launcherQueries.resources(orgId, {
|
...launcherQueries.resources(orgId, {
|
||||||
query: searchQuery,
|
query: config.query,
|
||||||
groupBy: config.groupBy,
|
groupBy: config.groupBy,
|
||||||
groupKey: group.groupKey,
|
groupKey: group.groupKey,
|
||||||
siteIds: config.siteIds,
|
siteIds: config.siteIds,
|
||||||
@@ -87,14 +96,33 @@ export function LauncherGroupSection({
|
|||||||
order: config.order,
|
order: config.order,
|
||||||
pageSize: 20
|
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 resources = data?.pages.flatMap((page) => page.resources) ?? [];
|
||||||
|
const showInitialLoader = isLoading && resources.length === 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = loadMoreRef.current;
|
const node = loadMoreRef.current;
|
||||||
if (!node || !hasNextPage) {
|
if (!node || !hasNextPage || !isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +137,7 @@ export function LauncherGroupSection({
|
|||||||
|
|
||||||
observer.observe(node);
|
observer.observe(node);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
}, [fetchNextPage, hasNextPage, isFetchingNextPage, isOpen]);
|
||||||
|
|
||||||
const groupTitle =
|
const groupTitle =
|
||||||
group.groupKey === "unlabeled"
|
group.groupKey === "unlabeled"
|
||||||
@@ -129,7 +157,7 @@ export function LauncherGroupSection({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<CollapsibleContent className="w-full">
|
<CollapsibleContent className="w-full">
|
||||||
{isLoading ? (
|
{showInitialLoader ? (
|
||||||
<div className="flex items-center justify-center py-10 text-muted-foreground">
|
<div className="flex items-center justify-center py-10 text-muted-foreground">
|
||||||
<Loader2 className="size-5 animate-spin" />
|
<Loader2 className="size-5 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,30 +14,31 @@ import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Label } from "@app/components/ui/label";
|
import { Label } from "@app/components/ui/label";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
import {
|
import {
|
||||||
readLauncherLastView,
|
readLauncherLastView,
|
||||||
writeLauncherLastView,
|
writeLauncherLastView,
|
||||||
type LauncherActiveViewId
|
type LauncherActiveViewId
|
||||||
} from "@app/lib/launcherLocalStorage";
|
} from "@app/lib/launcherLocalStorage";
|
||||||
|
import type { LauncherGroupResources } from "@app/lib/launcherServerData";
|
||||||
import {
|
import {
|
||||||
buildLauncherPath,
|
buildLauncherPath,
|
||||||
getLauncherUrlBaseConfig,
|
getLauncherUrlBaseConfig,
|
||||||
isLauncherConfigEqual,
|
isLauncherConfigEqual,
|
||||||
resolveLauncherStateFromUrl,
|
parseLauncherUrlState,
|
||||||
serializeLauncherUrlState
|
serializeLauncherUrlState
|
||||||
} from "@app/lib/launcherUrlState";
|
} from "@app/lib/launcherUrlState";
|
||||||
import { launcherQueries } from "@app/lib/queries";
|
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import {
|
import type {
|
||||||
defaultLauncherViewConfig,
|
LauncherGroup,
|
||||||
type LauncherViewConfig,
|
LauncherViewConfig,
|
||||||
type LauncherViewRecord
|
LauncherViewRecord
|
||||||
} from "@server/routers/launcher/types";
|
} 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 { Search } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import type { Selectedsite } from "@app/components/site-selector";
|
import type { Selectedsite } from "@app/components/site-selector";
|
||||||
@@ -52,33 +53,39 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|||||||
type ResourceLauncherProps = {
|
type ResourceLauncherProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isAdmin: boolean;
|
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({
|
export default function ResourceLauncher({
|
||||||
orgId,
|
orgId,
|
||||||
isAdmin
|
isAdmin,
|
||||||
|
views,
|
||||||
|
activeViewId,
|
||||||
|
config,
|
||||||
|
savedConfig,
|
||||||
|
groups,
|
||||||
|
groupsPagination,
|
||||||
|
resourcesByGroupKey
|
||||||
}: ResourceLauncherProps) {
|
}: ResourceLauncherProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const { navigate, isNavigating, searchParams } = useNavigationContext();
|
||||||
|
|
||||||
const [activeViewId, setActiveViewId] =
|
|
||||||
useState<LauncherActiveViewId>("default");
|
|
||||||
const hasRestoredLastView = useRef(false);
|
const hasRestoredLastView = useRef(false);
|
||||||
const isApplyingUrlRef = useRef(false);
|
|
||||||
|
|
||||||
const [config, setConfig] = useState<LauncherViewConfig>(
|
const [searchInput, setSearchInput] = useState(config.query);
|
||||||
defaultLauncherViewConfig
|
|
||||||
);
|
|
||||||
const [savedConfig, setSavedConfig] = useState<LauncherViewConfig>(
|
|
||||||
defaultLauncherViewConfig
|
|
||||||
);
|
|
||||||
const [searchInput, setSearchInput] = useState("");
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
const [newViewName, setNewViewName] = useState("");
|
const [newViewName, setNewViewName] = useState("");
|
||||||
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
||||||
@@ -90,68 +97,66 @@ export default function ResourceLauncher({
|
|||||||
const activeViewIdRef = useRef(activeViewId);
|
const activeViewIdRef = useRef(activeViewId);
|
||||||
activeViewIdRef.current = activeViewId;
|
activeViewIdRef.current = activeViewId;
|
||||||
|
|
||||||
const { data: views = [], isLoading: viewsLoading } = useQuery(
|
useEffect(() => {
|
||||||
launcherQueries.views(orgId)
|
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) => {
|
(viewId: LauncherActiveViewId, nextConfig: LauncherViewConfig) => {
|
||||||
if (isApplyingUrlRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = serializeLauncherUrlState({
|
const params = serializeLauncherUrlState({
|
||||||
viewId,
|
viewId,
|
||||||
config: nextConfig
|
config: nextConfig
|
||||||
});
|
});
|
||||||
const path = buildLauncherPath(orgId, params);
|
navigate({ searchParams: params });
|
||||||
router.replace(path, { scroll: false });
|
|
||||||
},
|
},
|
||||||
[orgId, router]
|
[navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedSyncSearch = useDebouncedCallback(
|
const debouncedNavigateSearch = useDebouncedCallback(
|
||||||
(viewId: LauncherActiveViewId, query: string) => {
|
(viewId: LauncherActiveViewId, query: string) => {
|
||||||
const nextConfig = { ...configRef.current, query };
|
navigateToConfig(viewId, { ...configRef.current, query });
|
||||||
setSearchQuery(query);
|
|
||||||
syncUrl(viewId, nextConfig);
|
|
||||||
},
|
},
|
||||||
300
|
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(
|
const selectView = useCallback(
|
||||||
(viewId: LauncherActiveViewId) => {
|
(viewId: LauncherActiveViewId) => {
|
||||||
writeLauncherLastView(orgId, viewId);
|
writeLauncherLastView(orgId, viewId);
|
||||||
const baseConfig = getLauncherUrlBaseConfig(viewId, views);
|
const baseConfig = getLauncherUrlBaseConfig(viewId, views);
|
||||||
syncUrl(viewId, baseConfig);
|
navigateToConfig(viewId, baseConfig);
|
||||||
},
|
},
|
||||||
[orgId, syncUrl, views]
|
[navigateToConfig, orgId, views]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeSavedView = useMemo(
|
const activeSavedView = useMemo(
|
||||||
@@ -186,12 +191,6 @@ export default function ResourceLauncher({
|
|||||||
[config.labelIds]
|
[config.labelIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const invalidateLauncher = () => {
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: ["ORG", orgId, "LAUNCHER"]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createViewMutation = useMutation({
|
const createViewMutation = useMutation({
|
||||||
mutationFn: async (payload: {
|
mutationFn: async (payload: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -202,23 +201,13 @@ export default function ResourceLauncher({
|
|||||||
return res.data.data as LauncherViewRecord;
|
return res.data.data as LauncherViewRecord;
|
||||||
},
|
},
|
||||||
onSuccess: (view) => {
|
onSuccess: (view) => {
|
||||||
invalidateLauncher();
|
|
||||||
writeLauncherLastView(orgId, view.viewId);
|
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({
|
const params = serializeLauncherUrlState({
|
||||||
viewId: view.viewId,
|
viewId: view.viewId,
|
||||||
config: view.config
|
config: view.config
|
||||||
});
|
});
|
||||||
router.replace(buildLauncherPath(orgId, params), { scroll: false });
|
navigate({ searchParams: params, replace: true });
|
||||||
|
router.refresh();
|
||||||
setSaveDialogOpen(false);
|
setSaveDialogOpen(false);
|
||||||
setNewViewName("");
|
setNewViewName("");
|
||||||
toast({
|
toast({
|
||||||
@@ -253,22 +242,12 @@ export default function ResourceLauncher({
|
|||||||
return res.data.data as LauncherViewRecord;
|
return res.data.data as LauncherViewRecord;
|
||||||
},
|
},
|
||||||
onSuccess: (view) => {
|
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({
|
const params = serializeLauncherUrlState({
|
||||||
viewId: view.viewId,
|
viewId: view.viewId,
|
||||||
config: view.config
|
config: view.config
|
||||||
});
|
});
|
||||||
router.replace(buildLauncherPath(orgId, params), { scroll: false });
|
navigate({ searchParams: params, replace: true });
|
||||||
|
router.refresh();
|
||||||
toast({
|
toast({
|
||||||
title: t("resourceLauncherViewSaved"),
|
title: t("resourceLauncherViewSaved"),
|
||||||
description: t("resourceLauncherViewSavedDescription")
|
description: t("resourceLauncherViewSavedDescription")
|
||||||
@@ -291,8 +270,13 @@ export default function ResourceLauncher({
|
|||||||
await api.delete(`/org/${orgId}/launcher/views/${viewId}`);
|
await api.delete(`/org/${orgId}/launcher/views/${viewId}`);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateLauncher();
|
writeLauncherLastView(orgId, "default");
|
||||||
selectView("default");
|
const params = serializeLauncherUrlState({
|
||||||
|
viewId: "default",
|
||||||
|
config: getLauncherUrlBaseConfig("default", views)
|
||||||
|
});
|
||||||
|
navigate({ searchParams: params, replace: true });
|
||||||
|
router.refresh();
|
||||||
toast({
|
toast({
|
||||||
title: t("resourceLauncherViewDeleted"),
|
title: t("resourceLauncherViewDeleted"),
|
||||||
description: t("resourceLauncherViewDeletedDescription")
|
description: t("resourceLauncherViewDeletedDescription")
|
||||||
@@ -317,9 +301,9 @@ export default function ResourceLauncher({
|
|||||||
...patch,
|
...patch,
|
||||||
query: searchInputRef.current
|
query: searchInputRef.current
|
||||||
};
|
};
|
||||||
syncUrl(activeViewIdRef.current, nextConfig);
|
navigateToConfig(activeViewIdRef.current, nextConfig);
|
||||||
},
|
},
|
||||||
[syncUrl]
|
[navigateToConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveToCurrent = () => {
|
const handleSaveToCurrent = () => {
|
||||||
@@ -370,7 +354,7 @@ export default function ResourceLauncher({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col" aria-busy={isNavigating}>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={t("resourceLauncherTitle")}
|
title={t("resourceLauncherTitle")}
|
||||||
description={t("resourceLauncherDescription")}
|
description={t("resourceLauncherDescription")}
|
||||||
@@ -386,7 +370,7 @@ export default function ResourceLauncher({
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
setSearchInput(value);
|
setSearchInput(value);
|
||||||
debouncedSyncSearch(
|
debouncedNavigateSearch(
|
||||||
activeViewIdRef.current,
|
activeViewIdRef.current,
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
@@ -397,16 +381,14 @@ export default function ResourceLauncher({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!viewsLoading ? (
|
<LauncherViewTabs
|
||||||
<LauncherViewTabs
|
activeViewId={activeViewId}
|
||||||
activeViewId={activeViewId}
|
savedViews={views.map((view) => ({
|
||||||
savedViews={views.map((view) => ({
|
viewId: view.viewId,
|
||||||
viewId: view.viewId,
|
name: view.name
|
||||||
name: view.name
|
}))}
|
||||||
}))}
|
onSelectView={selectView}
|
||||||
onSelectView={selectView}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0 justify-end">
|
<div className="flex items-center gap-2 shrink-0 justify-end">
|
||||||
<LauncherSaveViewMenu
|
<LauncherSaveViewMenu
|
||||||
@@ -462,8 +444,10 @@ export default function ResourceLauncher({
|
|||||||
<LauncherGroupList
|
<LauncherGroupList
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
activeViewId={activeViewId}
|
activeViewId={activeViewId}
|
||||||
config={{ ...config, query: searchQuery }}
|
config={config}
|
||||||
searchQuery={searchQuery}
|
initialGroups={groups}
|
||||||
|
groupsPagination={groupsPagination}
|
||||||
|
resourcesByGroupKey={resourcesByGroupKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||||
|
|||||||
43
src/lib/launcherSearchParams.ts
Normal file
43
src/lib/launcherSearchParams.ts
Normal 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;
|
||||||
|
}
|
||||||
128
src/lib/launcherServerData.ts
Normal file
128
src/lib/launcherServerData.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -53,6 +53,11 @@ import type {
|
|||||||
LauncherListQuery,
|
LauncherListQuery,
|
||||||
LauncherViewConfig
|
LauncherViewConfig
|
||||||
} from "@server/routers/launcher/types";
|
} 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 = {
|
export type ProductUpdate = {
|
||||||
link: string | null;
|
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 = {
|
export const launcherQueries = {
|
||||||
views: (orgId: string) =>
|
views: (orgId: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
@@ -1228,7 +1194,7 @@ export const launcherQueries = {
|
|||||||
infiniteQueryOptions({
|
infiniteQueryOptions({
|
||||||
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
|
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
|
||||||
queryFn: async ({ pageParam = 1, signal, meta }) => {
|
queryFn: async ({ pageParam = 1, signal, meta }) => {
|
||||||
const sp = launcherSearchParams(filters, pageParam);
|
const sp = buildLauncherSearchParams(filters, pageParam);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListLauncherGroupsResponse>
|
AxiosResponse<ListLauncherGroupsResponse>
|
||||||
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, { signal });
|
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, { signal });
|
||||||
@@ -1249,7 +1215,7 @@ export const launcherQueries = {
|
|||||||
infiniteQueryOptions({
|
infiniteQueryOptions({
|
||||||
queryKey: ["ORG", orgId, "LAUNCHER", "RESOURCES", filters] as const,
|
queryKey: ["ORG", orgId, "LAUNCHER", "RESOURCES", filters] as const,
|
||||||
queryFn: async ({ pageParam = 1, signal, meta }) => {
|
queryFn: async ({ pageParam = 1, signal, meta }) => {
|
||||||
const sp = launcherSearchParams(filters, pageParam);
|
const sp = buildLauncherSearchParams(filters, pageParam);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListLauncherResourcesResponse>
|
AxiosResponse<ListLauncherResourcesResponse>
|
||||||
>(`/org/${orgId}/launcher/resources?${sp.toString()}`, {
|
>(`/org/${orgId}/launcher/resources?${sp.toString()}`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user