(
- 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 (
-
+
{
const value = event.target.value;
setSearchInput(value);
- debouncedSyncSearch(
+ debouncedNavigateSearch(
activeViewIdRef.current,
value
);
@@ -397,16 +381,14 @@ export default function ResourceLauncher({
className="pl-8"
/>
- {!viewsLoading ? (
-
({
- viewId: view.viewId,
- name: view.name
- }))}
- onSelectView={selectView}
- />
- ) : null}
+ ({
+ viewId: view.viewId,
+ name: view.name
+ }))}
+ onSelectView={selectView}
+ />
diff --git a/src/lib/launcherSearchParams.ts b/src/lib/launcherSearchParams.ts
new file mode 100644
index 000000000..acaccd06f
--- /dev/null
+++ b/src/lib/launcherSearchParams.ts
@@ -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;
+}
diff --git a/src/lib/launcherServerData.ts b/src/lib/launcherServerData.ts
new file mode 100644
index 000000000..da8fb1846
--- /dev/null
+++ b/src/lib/launcherServerData.ts
@@ -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;
+};
+
+const emptyResources: LauncherGroupResources = {
+ resources: [],
+ pagination: { total: 0, page: 1, pageSize: 20 }
+};
+
+export async function fetchLauncherPageData(
+ orgId: string,
+ searchParams: URLSearchParams,
+ cookieHeader: Awaited<
+ ReturnType
+ >
+): Promise {
+ let views: LauncherViewRecord[] = [];
+ try {
+ const viewsRes = await internal.get<
+ AxiosResponse
+ >(`/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
+ >(`/org/${orgId}/launcher/groups?${sp.toString()}`, cookieHeader);
+ groups = groupsRes.data.data.groups;
+ groupsPagination = groupsRes.data.data.pagination;
+ } catch (e) {}
+
+ const resourcesByGroupKey: Record = {};
+
+ await Promise.all(
+ groups.map(async (group) => {
+ try {
+ const sp = buildLauncherSearchParams(
+ {
+ ...groupFilters,
+ groupKey: group.groupKey
+ },
+ 1
+ );
+ const res = await internal.get<
+ AxiosResponse
+ >(
+ `/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
+ };
+}
diff --git a/src/lib/queries.ts b/src/lib/queries.ts
index 4d1cdc75c..c5d613942 100644
--- a/src/lib/queries.ts
+++ b/src/lib/queries.ts
@@ -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
>(`/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
>(`/org/${orgId}/launcher/resources?${sp.toString()}`, {