mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-08 07:05:08 +02:00
821 lines
28 KiB
TypeScript
821 lines
28 KiB
TypeScript
"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 {
|
|
buildLauncherPath,
|
|
getLauncherUrlBaseConfig,
|
|
isLauncherConfigEqual,
|
|
parseLauncherUrlState,
|
|
serializeLauncherUrlState
|
|
} from "@app/lib/launcherUrlState";
|
|
import { useToast } from "@app/hooks/useToast";
|
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
import {
|
|
getEffectiveLauncherConfig,
|
|
shouldShowFlatResourceList,
|
|
shouldShowLauncherGroupList,
|
|
shouldShowSearchFirstGate
|
|
} from "@app/lib/launcherScale";
|
|
import { launcherQueries } from "@app/lib/queries";
|
|
import {
|
|
getEffectiveDefaultLauncherConfig,
|
|
type LauncherDefaultViewOverrides,
|
|
type LauncherGroup,
|
|
type LauncherResource,
|
|
type LauncherScaleInfo,
|
|
type LauncherViewConfig,
|
|
type LauncherViewRecord
|
|
} from "@server/routers/launcher/types";
|
|
import { useMutation, useQuery, useQueryClient } 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,
|
|
useTransition
|
|
} from "react";
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
import type { Selectedsite } from "@app/components/site-selector";
|
|
import type { SelectedLabel } from "@app/components/labels-selector";
|
|
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
|
import { cn } from "@app/lib/cn";
|
|
import { LauncherFilterPopover } from "./LauncherFilterPopover";
|
|
import { LauncherFlatResourceList } from "./LauncherFlatResourceList";
|
|
import { LauncherGroupList } from "./LauncherGroupList";
|
|
import { LauncherSearchFirstGate } from "./LauncherSearchFirstGate";
|
|
import { LauncherRefreshButton } from "./LauncherRefreshButton";
|
|
import { LauncherResourcePanel } from "./LauncherResourcePanel";
|
|
import { LauncherSettingsMenu } from "./LauncherSettingsMenu";
|
|
import { LauncherSortButton } from "./LauncherSortButton";
|
|
import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs";
|
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
|
|
type ResourceLauncherProps = {
|
|
orgId: string;
|
|
isAdmin: boolean;
|
|
views: LauncherViewRecord[];
|
|
defaultViewOverrides: LauncherDefaultViewOverrides;
|
|
activeViewId: LauncherActiveViewId;
|
|
config: LauncherViewConfig;
|
|
savedConfig: LauncherViewConfig;
|
|
scale: LauncherScaleInfo;
|
|
groups: LauncherGroup[];
|
|
groupsPagination: {
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
};
|
|
};
|
|
|
|
export default function ResourceLauncher({
|
|
orgId,
|
|
isAdmin,
|
|
views,
|
|
defaultViewOverrides,
|
|
activeViewId,
|
|
config,
|
|
savedConfig,
|
|
scale: initialScale,
|
|
groups,
|
|
groupsPagination
|
|
}: ResourceLauncherProps) {
|
|
const t = useTranslations();
|
|
const { toast } = useToast();
|
|
const { env } = useEnvContext();
|
|
const api = createApiClient({ env });
|
|
const queryClient = useQueryClient();
|
|
const router = useRouter();
|
|
const { navigate, isNavigating, searchParams } = useNavigationContext();
|
|
const [isRefreshing, startRefreshTransition] = useTransition();
|
|
const hasRestoredLastView = useRef(false);
|
|
|
|
const [searchInputResetKey, setSearchInputResetKey] = useState(0);
|
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [newViewName, setNewViewName] = useState("");
|
|
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
|
const [selectedResource, setSelectedResource] =
|
|
useState<LauncherResource | null>(null);
|
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
|
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
|
|
const scaleFilters = useMemo(
|
|
() => ({
|
|
query: config.query,
|
|
groupBy: config.groupBy,
|
|
siteIds: config.siteIds,
|
|
labelIds: config.labelIds,
|
|
sort_by: config.sortBy,
|
|
order: config.order
|
|
}),
|
|
[
|
|
config.groupBy,
|
|
config.labelIds,
|
|
config.order,
|
|
config.query,
|
|
config.siteIds,
|
|
config.sortBy
|
|
]
|
|
);
|
|
|
|
const { data: scale = initialScale } = useQuery({
|
|
...launcherQueries.scale(orgId, scaleFilters),
|
|
initialData: initialScale
|
|
});
|
|
|
|
const showGroupList = shouldShowLauncherGroupList(scale, config);
|
|
const showSearchFirstGate = shouldShowSearchFirstGate(scale, config);
|
|
const showFlatResourceList = shouldShowFlatResourceList(scale, config);
|
|
const effectiveConfig = useMemo(
|
|
() => getEffectiveLauncherConfig(scale, config),
|
|
[config, scale]
|
|
);
|
|
|
|
const configRef = useRef(config);
|
|
configRef.current = config;
|
|
const searchInputRef = useRef(config.query);
|
|
const activeViewIdRef = useRef(activeViewId);
|
|
activeViewIdRef.current = activeViewId;
|
|
|
|
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,
|
|
defaultViewOverrides
|
|
);
|
|
const params = serializeLauncherUrlState({
|
|
viewId: lastView,
|
|
config: baseConfig
|
|
});
|
|
navigate({ searchParams: params, replace: true });
|
|
}, [
|
|
activeViewId,
|
|
defaultViewOverrides,
|
|
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,
|
|
defaultViewOverrides
|
|
);
|
|
navigateToConfig(viewId, baseConfig);
|
|
},
|
|
[defaultViewOverrides, 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 canResetSystemDefault =
|
|
isDefaultView &&
|
|
(Boolean(defaultViewOverrides.personal) ||
|
|
(isAdmin && Boolean(defaultViewOverrides.orgWide)));
|
|
|
|
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 saveDefaultViewMutation = useMutation({
|
|
mutationFn: async (payload: {
|
|
config: LauncherViewConfig;
|
|
orgWide: boolean;
|
|
}) => {
|
|
const res = await api.put(
|
|
`/org/${orgId}/launcher/default-view`,
|
|
payload
|
|
);
|
|
return res.data.data as LauncherViewRecord;
|
|
},
|
|
onSuccess: () => {
|
|
writeLauncherLastView(orgId, "default");
|
|
const params = serializeLauncherUrlState({
|
|
viewId: "default",
|
|
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 resetSystemDefaultMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const resetAll = isAdmin && Boolean(defaultViewOverrides.orgWide);
|
|
await api.delete(`/org/${orgId}/launcher/default-view`, {
|
|
data: resetAll ? { all: true } : { orgWide: false }
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
writeLauncherLastView(orgId, "default");
|
|
const nextOverrides: LauncherDefaultViewOverrides = {
|
|
personal: null,
|
|
orgWide:
|
|
isAdmin && defaultViewOverrides.orgWide
|
|
? null
|
|
: defaultViewOverrides.orgWide
|
|
};
|
|
const targetConfig =
|
|
getEffectiveDefaultLauncherConfig(nextOverrides);
|
|
searchInputRef.current = targetConfig.query;
|
|
setSearchInputResetKey((key) => key + 1);
|
|
const params = serializeLauncherUrlState({
|
|
viewId: "default",
|
|
config: targetConfig
|
|
});
|
|
navigate({ searchParams: params, replace: true });
|
|
router.refresh();
|
|
toast({
|
|
title: t("resourceLauncherSystemDefaultRestored"),
|
|
description: t(
|
|
"resourceLauncherSystemDefaultRestoredDescription"
|
|
)
|
|
});
|
|
},
|
|
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,
|
|
defaultViewOverrides
|
|
)
|
|
});
|
|
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<LauncherViewConfig>) => {
|
|
const nextConfig = {
|
|
...configRef.current,
|
|
...patch,
|
|
query: searchInputRef.current
|
|
};
|
|
navigateToConfig(activeViewIdRef.current, nextConfig);
|
|
},
|
|
[navigateToConfig]
|
|
);
|
|
|
|
const handleClearFilters = useCallback(() => {
|
|
searchInputRef.current = "";
|
|
setSearchInputResetKey((key) => key + 1);
|
|
navigateToConfig(activeViewIdRef.current, {
|
|
...configRef.current,
|
|
query: "",
|
|
siteIds: [],
|
|
labelIds: []
|
|
});
|
|
}, [navigateToConfig]);
|
|
|
|
const handleResetView = useCallback(() => {
|
|
searchInputRef.current = savedConfig.query;
|
|
setSearchInputResetKey((key) => key + 1);
|
|
navigateToConfig(activeViewIdRef.current, savedConfig);
|
|
}, [navigateToConfig, savedConfig]);
|
|
|
|
const handleResetSystemDefault = useCallback(() => {
|
|
resetSystemDefaultMutation.mutate();
|
|
}, [resetSystemDefaultMutation]);
|
|
|
|
const refreshData = () => {
|
|
startRefreshTransition(async () => {
|
|
try {
|
|
await api.post(`/org/${orgId}/launcher/invalidate-cache`);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["ORG", orgId, "LAUNCHER"]
|
|
});
|
|
router.refresh();
|
|
} catch {
|
|
toast({
|
|
title: t("error"),
|
|
description: t("refreshError"),
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleSaveToCurrent = () => {
|
|
if (isDefaultView) {
|
|
saveDefaultViewMutation.mutate({
|
|
config,
|
|
orgWide: false
|
|
});
|
|
return;
|
|
}
|
|
if (isOrgWideView && !isAdmin) {
|
|
return;
|
|
}
|
|
updateViewMutation.mutate({
|
|
viewId: activeViewId,
|
|
config
|
|
});
|
|
};
|
|
|
|
const handleSaveAsNew = () => {
|
|
setSaveOrgWide(false);
|
|
setNewViewName("");
|
|
setSaveDialogOpen(true);
|
|
};
|
|
|
|
const handleSaveForEveryone = () => {
|
|
if (isDefaultView) {
|
|
saveDefaultViewMutation.mutate({
|
|
config,
|
|
orgWide: true
|
|
});
|
|
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
|
|
});
|
|
};
|
|
|
|
const handleResourceSelect = useCallback((resource: LauncherResource) => {
|
|
setSelectedResource(resource);
|
|
setPanelOpen(true);
|
|
}, []);
|
|
|
|
const handlePanelOpenChange = useCallback((open: boolean) => {
|
|
setPanelOpen(open);
|
|
if (!open) {
|
|
setSelectedResource(null);
|
|
}
|
|
}, []);
|
|
|
|
const savedViewTabs = views.map((view) => ({
|
|
viewId: view.viewId,
|
|
name: view.name
|
|
}));
|
|
|
|
const renderToolbarSearch = (searchClassName: string) => (
|
|
<div className={cn("relative shrink-0", searchClassName)}>
|
|
<span className="pointer-events-none absolute inset-y-0 left-2 flex items-center">
|
|
<Search className="size-4 text-muted-foreground" />
|
|
</span>
|
|
<Input
|
|
key={`${activeViewId}-${searchInputResetKey}`}
|
|
defaultValue={config.query}
|
|
onChange={(event) => {
|
|
const value = event.currentTarget.value;
|
|
searchInputRef.current = value;
|
|
debouncedNavigateSearch(activeViewIdRef.current, value);
|
|
}}
|
|
placeholder={t("resourceLauncherSearchPlaceholder")}
|
|
className="pl-8"
|
|
type="search"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const renderToolbarFilterSort = () => (
|
|
<>
|
|
<LauncherFilterPopover
|
|
orgId={orgId}
|
|
selectedSites={selectedSites}
|
|
selectedLabels={selectedLabels}
|
|
onSitesChange={(sites) =>
|
|
applyConfigPatch({
|
|
siteIds: sites.map((site) => site.siteId)
|
|
})
|
|
}
|
|
onLabelsChange={(labels) =>
|
|
applyConfigPatch({
|
|
labelIds: labels.map((label) => label.labelId)
|
|
})
|
|
}
|
|
/>
|
|
<LauncherSortButton
|
|
order={config.order}
|
|
onToggle={() =>
|
|
applyConfigPatch({
|
|
order: config.order === "asc" ? "desc" : "asc"
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
const renderToolbarActions = () => (
|
|
<>
|
|
<LauncherSaveViewMenu
|
|
isDefaultView={isDefaultView}
|
|
isAdmin={isAdmin}
|
|
isOrgWideView={isOrgWideView}
|
|
hasUnsavedChanges={hasUnsavedChanges}
|
|
canResetSystemDefault={canResetSystemDefault}
|
|
onSaveToCurrent={handleSaveToCurrent}
|
|
onSaveAsNew={handleSaveAsNew}
|
|
onSaveForEveryone={handleSaveForEveryone}
|
|
onMakePersonal={handleMakePersonal}
|
|
onResetView={handleResetView}
|
|
onResetSystemDefault={handleResetSystemDefault}
|
|
onDeleteView={() => setDeleteDialogOpen(true)}
|
|
/>
|
|
<LauncherSettingsMenu
|
|
config={config}
|
|
capabilities={scale.capabilities}
|
|
isCompactMode={scale.mode === "compact"}
|
|
selectedGroupBy={effectiveConfig.groupBy}
|
|
onConfigChange={applyConfigPatch}
|
|
/>
|
|
<LauncherRefreshButton
|
|
onRefresh={refreshData}
|
|
isRefreshing={isRefreshing || isNavigating}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
const renderToolbarViews = () => (
|
|
<LauncherViewTabs
|
|
activeViewId={activeViewId}
|
|
savedViews={savedViewTabs}
|
|
onSelectView={selectView}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-col" aria-busy={isNavigating}>
|
|
<SettingsSectionTitle
|
|
title={t("resourceLauncherTitle")}
|
|
description={t("resourceLauncherDescription")}
|
|
/>
|
|
|
|
{isDesktop ? (
|
|
<div className="mb-6 flex w-full min-w-0 items-center gap-3">
|
|
{renderToolbarSearch("w-64")}
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
{renderToolbarFilterSort()}
|
|
</div>
|
|
<div className="min-w-0 flex-1 overflow-x-auto">
|
|
{renderToolbarViews()}
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
{renderToolbarActions()}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="mb-6 flex flex-col gap-3">
|
|
<div className="flex items-center gap-2 overflow-x-auto">
|
|
{renderToolbarActions()}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
{renderToolbarSearch("w-full")}
|
|
</div>
|
|
{renderToolbarFilterSort()}
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
{renderToolbarViews()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showSearchFirstGate ? (
|
|
<LauncherSearchFirstGate layout={config.layout} />
|
|
) : showGroupList ? (
|
|
<LauncherGroupList
|
|
orgId={orgId}
|
|
activeViewId={activeViewId}
|
|
config={effectiveConfig}
|
|
initialGroups={groups}
|
|
groupsPagination={groupsPagination}
|
|
onClearFilters={handleClearFilters}
|
|
onResourceSelect={handleResourceSelect}
|
|
/>
|
|
) : showFlatResourceList ? (
|
|
<LauncherFlatResourceList
|
|
orgId={orgId}
|
|
activeViewId={activeViewId}
|
|
config={effectiveConfig}
|
|
onClearFilters={handleClearFilters}
|
|
onResourceSelect={handleResourceSelect}
|
|
/>
|
|
) : null}
|
|
|
|
<LauncherResourcePanel
|
|
open={panelOpen}
|
|
onOpenChange={handlePanelOpenChange}
|
|
resource={selectedResource}
|
|
orgId={orgId}
|
|
isAdmin={isAdmin}
|
|
/>
|
|
|
|
{activeSavedView ? (
|
|
<ConfirmDeleteDialog
|
|
open={deleteDialogOpen}
|
|
setOpen={setDeleteDialogOpen}
|
|
string={activeSavedView.name}
|
|
title={t("resourceLauncherDeleteViewTitle")}
|
|
buttonText={t("resourceLauncherDeleteViewConfirm")}
|
|
dialog={<p>{t("resourceLauncherDeleteViewQuestion")}</p>}
|
|
onConfirm={async () => {
|
|
await deleteViewMutation.mutateAsync(
|
|
activeSavedView.viewId
|
|
);
|
|
}}
|
|
/>
|
|
) : null}
|
|
|
|
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
|
<CredenzaContent>
|
|
<CredenzaHeader>
|
|
<CredenzaTitle>
|
|
{t("resourceLauncherSaveAsNewView")}
|
|
</CredenzaTitle>
|
|
<CredenzaDescription>
|
|
{t("resourceLauncherSaveAsNewViewDescription")}
|
|
</CredenzaDescription>
|
|
</CredenzaHeader>
|
|
<CredenzaBody>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="new-view-name">
|
|
{t("resourceLauncherViewNameLabel")}
|
|
</Label>
|
|
<Input
|
|
id="new-view-name"
|
|
value={newViewName}
|
|
onChange={(event) =>
|
|
setNewViewName(event.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
{isAdmin ? (
|
|
<div className="mt-4">
|
|
<CheckboxWithLabel
|
|
id="save-org-wide"
|
|
aria-describedby="save-org-wide-desc"
|
|
label={t("resourceLauncherSaveForEveryone")}
|
|
checked={saveOrgWide}
|
|
onCheckedChange={(checked) =>
|
|
setSaveOrgWide(checked === true)
|
|
}
|
|
/>
|
|
<p
|
|
id="save-org-wide-desc"
|
|
className="text-sm text-muted-foreground mt-2"
|
|
>
|
|
{t(
|
|
"resourceLauncherSaveForEveryoneDescription"
|
|
)}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</CredenzaBody>
|
|
<CredenzaFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setSaveDialogOpen(false)}
|
|
>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleCreateView}
|
|
loading={createViewMutation.isPending}
|
|
>
|
|
{t("save")}
|
|
</Button>
|
|
</CredenzaFooter>
|
|
</CredenzaContent>
|
|
</Credenza>
|
|
</div>
|
|
);
|
|
}
|