"use client"; import { Credenza, CredenzaBody, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; 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, parseLauncherUrlState, serializeLauncherUrlState } from "@app/lib/launcherUrlState"; import { useToast } from "@app/hooks/useToast"; import { useEnvContext } from "@app/hooks/useEnvContext"; import type { LauncherGroup, LauncherViewConfig, LauncherViewRecord } from "@server/routers/launcher/types"; import { useMutation } from "@tanstack/react-query"; import { Search } from "lucide-react"; import { useTranslations } from "next-intl"; 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"; import type { SelectedLabel } from "@app/components/labels-selector"; import { LauncherFilterPopover } from "./LauncherFilterPopover"; import { LauncherGroupList } from "./LauncherGroupList"; import { LauncherSettingsMenu } from "./LauncherSettingsMenu"; import { LauncherSortButton } from "./LauncherSortButton"; import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs"; 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; }; export default function ResourceLauncher({ orgId, isAdmin, views, activeViewId, config, savedConfig, groups, groupsPagination, resourcesByGroupKey }: ResourceLauncherProps) { const t = useTranslations(); const { toast } = useToast(); const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { navigate, isNavigating, searchParams } = useNavigationContext(); const hasRestoredLastView = useRef(false); const [searchInput, setSearchInput] = useState(config.query); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const [newViewName, setNewViewName] = useState(""); const [saveOrgWide, setSaveOrgWide] = useState(false); const configRef = useRef(config); configRef.current = config; const searchInputRef = useRef(searchInput); searchInputRef.current = searchInput; const activeViewIdRef = useRef(activeViewId); activeViewIdRef.current = activeViewId; useEffect(() => { setSearchInput(config.query); }, [config.query]); 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) => { const params = serializeLauncherUrlState({ viewId, config: nextConfig }); navigate({ searchParams: params }); }, [navigate] ); const debouncedNavigateSearch = useDebouncedCallback( (viewId: LauncherActiveViewId, query: string) => { navigateToConfig(viewId, { ...configRef.current, query }); }, 300 ); const selectView = useCallback( (viewId: LauncherActiveViewId) => { writeLauncherLastView(orgId, viewId); const baseConfig = getLauncherUrlBaseConfig(viewId, views); navigateToConfig(viewId, baseConfig); }, [navigateToConfig, orgId, views] ); const activeSavedView = useMemo( () => activeViewId === "default" ? null : views.find((view) => view.viewId === activeViewId), [activeViewId, views] ); const isDefaultView = activeViewId === "default"; const isOrgWideView = Boolean(activeSavedView?.isOrgWide); const hasUnsavedChanges = !isLauncherConfigEqual(config, savedConfig); const selectedSites: Selectedsite[] = useMemo( () => config.siteIds.map((siteId) => ({ siteId, name: String(siteId), type: "newt" })), [config.siteIds] ); const selectedLabels: SelectedLabel[] = useMemo( () => config.labelIds.map((labelId) => ({ labelId, name: String(labelId), color: "#a1a1aa" })), [config.labelIds] ); const createViewMutation = useMutation({ mutationFn: async (payload: { name: string; config: LauncherViewConfig; orgWide: boolean; }) => { const res = await api.post(`/org/${orgId}/launcher/views`, payload); return res.data.data as LauncherViewRecord; }, onSuccess: (view) => { writeLauncherLastView(orgId, view.viewId); const params = serializeLauncherUrlState({ viewId: view.viewId, config: view.config }); navigate({ searchParams: params, replace: true }); router.refresh(); setSaveDialogOpen(false); setNewViewName(""); toast({ title: t("resourceLauncherViewSaved"), description: t("resourceLauncherViewSavedDescription") }); }, onError: (error) => { toast({ variant: "destructive", title: t("resourceLauncherViewSaveFailed"), description: formatAxiosError( error, t("resourceLauncherViewSaveFailedDescription") ) }); } }); const updateViewMutation = useMutation({ mutationFn: async (payload: { viewId: number; name?: string; config?: LauncherViewConfig; orgWide?: boolean; }) => { const { viewId, ...body } = payload; const res = await api.put( `/org/${orgId}/launcher/views/${viewId}`, body ); return res.data.data as LauncherViewRecord; }, onSuccess: (view) => { const params = serializeLauncherUrlState({ viewId: view.viewId, config: view.config }); navigate({ searchParams: params, replace: true }); router.refresh(); toast({ title: t("resourceLauncherViewSaved"), description: t("resourceLauncherViewSavedDescription") }); }, onError: (error) => { toast({ variant: "destructive", title: t("resourceLauncherViewSaveFailed"), description: formatAxiosError( error, t("resourceLauncherViewSaveFailedDescription") ) }); } }); const deleteViewMutation = useMutation({ mutationFn: async (viewId: number) => { await api.delete(`/org/${orgId}/launcher/views/${viewId}`); }, onSuccess: () => { 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") }); }, onError: (error) => { toast({ variant: "destructive", title: t("resourceLauncherViewDeleteFailed"), description: formatAxiosError( error, t("resourceLauncherViewDeleteFailedDescription") ) }); } }); const applyConfigPatch = useCallback( (patch: Partial) => { const nextConfig = { ...configRef.current, ...patch, query: searchInputRef.current }; navigateToConfig(activeViewIdRef.current, nextConfig); }, [navigateToConfig] ); const handleClearFilters = useCallback(() => { setSearchInput(""); navigateToConfig(activeViewIdRef.current, { ...configRef.current, query: "", siteIds: [], labelIds: [] }); }, [navigateToConfig]); const handleSaveToCurrent = () => { if (isDefaultView) { return; } updateViewMutation.mutate({ viewId: activeViewId, config }); }; const handleSaveAsNew = () => { setSaveOrgWide(false); setNewViewName(""); setSaveDialogOpen(true); }; const handleSaveForEveryone = () => { if (isDefaultView) { return; } updateViewMutation.mutate({ viewId: activeViewId, orgWide: true }); }; const handleMakePersonal = () => { if (isDefaultView) { return; } updateViewMutation.mutate({ viewId: activeViewId, orgWide: false }); }; const handleCreateView = () => { if (!newViewName.trim()) { return; } createViewMutation.mutate({ name: newViewName.trim(), config, orgWide: saveOrgWide && isAdmin }); }; return (
{ const value = event.target.value; setSearchInput(value); debouncedNavigateSearch( activeViewIdRef.current, value ); }} placeholder={t( "resourceLauncherSearchPlaceholder" )} className="pl-8" />
({ viewId: view.viewId, name: view.name }))} onSelectView={selectView} />
applyConfigPatch({ siteIds: sites.map((site) => site.siteId) }) } onLabelsChange={(labels) => applyConfigPatch({ labelIds: labels.map( (label) => label.labelId ) }) } /> applyConfigPatch({ order: config.order === "asc" ? "desc" : "asc" }) } /> { if (!isDefaultView) { deleteViewMutation.mutate(activeViewId); } }} />
{t("resourceLauncherSaveAsNewView")} {t("resourceLauncherSaveAsNewViewDescription")}
setNewViewName(event.target.value) } />
{isAdmin ? (
setSaveOrgWide(checked === true) } />

{t( "resourceLauncherSaveForEveryoneDescription" )}

) : null}
); }