mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 06:39:52 +02:00
open side panel on click
This commit is contained in:
@@ -3573,6 +3573,7 @@
|
|||||||
"resourceLauncherShowRecents": "Show Recents",
|
"resourceLauncherShowRecents": "Show Recents",
|
||||||
"resourceLauncherDeleteView": "Delete View",
|
"resourceLauncherDeleteView": "Delete View",
|
||||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||||
|
"resourceLauncherResourceDetailsDescription": "View details for this resource.",
|
||||||
"resourceLauncherUnlabeled": "Unlabeled",
|
"resourceLauncherUnlabeled": "Unlabeled",
|
||||||
"resourceLauncherNoSite": "No Site",
|
"resourceLauncherNoSite": "No Site",
|
||||||
"resourceLauncherNoResourcesInGroup": "No resources in this group",
|
"resourceLauncherNoResourcesInGroup": "No resources in this group",
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ async function mapPublicResources(
|
|||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
|
niceId: resources.niceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
mode: resources.mode,
|
mode: resources.mode,
|
||||||
fullDomain: resources.fullDomain,
|
fullDomain: resources.fullDomain,
|
||||||
@@ -842,6 +843,7 @@ async function mapPublicResources(
|
|||||||
launcherResourceKey: key,
|
launcherResourceKey: key,
|
||||||
resourceType: "public",
|
resourceType: "public",
|
||||||
resourceId: row.resourceId,
|
resourceId: row.resourceId,
|
||||||
|
niceId: row.niceId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
...access,
|
...access,
|
||||||
iconUrl: null,
|
iconUrl: null,
|
||||||
@@ -876,6 +878,7 @@ async function mapSiteResources(
|
|||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
siteResourceId: siteResources.siteResourceId,
|
siteResourceId: siteResources.siteResourceId,
|
||||||
|
niceId: siteResources.niceId,
|
||||||
name: siteResources.name,
|
name: siteResources.name,
|
||||||
mode: siteResources.mode,
|
mode: siteResources.mode,
|
||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
@@ -934,6 +937,7 @@ async function mapSiteResources(
|
|||||||
resourceType: "site",
|
resourceType: "site",
|
||||||
resourceId: row.siteResourceId,
|
resourceId: row.siteResourceId,
|
||||||
siteResourceId: row.siteResourceId,
|
siteResourceId: row.siteResourceId,
|
||||||
|
niceId: row.niceId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
...access,
|
...access,
|
||||||
iconUrl: null,
|
iconUrl: null,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type LauncherResource = {
|
|||||||
resourceType: "public" | "site";
|
resourceType: "public" | "site";
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteResourceId?: number;
|
siteResourceId?: number;
|
||||||
|
niceId: string;
|
||||||
name: string;
|
name: string;
|
||||||
accessDisplay: string;
|
accessDisplay: string;
|
||||||
accessCopyValue: string;
|
accessCopyValue: string;
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetPortal,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger
|
||||||
|
} from "./ui/sheet";
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
|
|
||||||
|
type BaseProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RootSidePanelProps = BaseProps & {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidePanelProps = {
|
||||||
|
className?: string;
|
||||||
|
asChild?: true;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const desktop = "(min-width: 768px)";
|
||||||
|
|
||||||
|
const SidePanel = ({ children, ...props }: RootSidePanelProps) => {
|
||||||
|
return <Sheet {...props}>{children}</Sheet>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelTrigger = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetTrigger className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</SheetTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelClose = ({ className, children, ...props }: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetClose className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</SheetClose>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelContent = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SidePanelProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 flex min-h-0 flex-col gap-4 overflow-hidden border bg-card px-6 pt-6 pb-1 shadow-lg transition ease-in-out",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
|
||||||
|
"data-[state=closed]:duration-200 data-[state=open]:duration-300",
|
||||||
|
isDesktop
|
||||||
|
? "inset-y-0 right-0 h-full w-2/5 border-l"
|
||||||
|
: "inset-x-0 bottom-0 max-h-[85dvh] w-full border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelDescription = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetDescription className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</SheetDescription>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelHeader = ({ className, children, ...props }: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetHeader
|
||||||
|
className={cn("shrink-0 -mx-6 px-6", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SheetHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelTitle = ({ className, children, ...props }: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetTitle className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</SheetTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelBody = ({ className, children, ...props }: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">{children}</div>
|
||||||
|
<div
|
||||||
|
className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidePanelFooter = ({ className, children, ...props }: SidePanelProps) => {
|
||||||
|
return (
|
||||||
|
<SheetFooter
|
||||||
|
className={cn(
|
||||||
|
"-mt-4 shrink-0 border-t border-border py-4 -mx-6 gap-2 px-6 bg-card",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SheetFooter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
SidePanel,
|
||||||
|
SidePanelBody,
|
||||||
|
SidePanelClose,
|
||||||
|
SidePanelContent,
|
||||||
|
SidePanelDescription,
|
||||||
|
SidePanelFooter,
|
||||||
|
SidePanelHeader,
|
||||||
|
SidePanelTitle,
|
||||||
|
SidePanelTrigger
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import type { LauncherGroupResources } from "@app/lib/launcherServerData";
|
|||||||
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";
|
||||||
@@ -25,6 +26,7 @@ type LauncherGroupListProps = {
|
|||||||
};
|
};
|
||||||
resourcesByGroupKey: Record<string, LauncherGroupResources>;
|
resourcesByGroupKey: Record<string, LauncherGroupResources>;
|
||||||
onClearFilters?: () => void;
|
onClearFilters?: () => void;
|
||||||
|
onResourceSelect: (resource: LauncherResource) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function hasActiveLauncherFilters(config: LauncherViewConfig): boolean {
|
function hasActiveLauncherFilters(config: LauncherViewConfig): boolean {
|
||||||
@@ -42,7 +44,8 @@ export function LauncherGroupList({
|
|||||||
initialGroups,
|
initialGroups,
|
||||||
groupsPagination,
|
groupsPagination,
|
||||||
resourcesByGroupKey,
|
resourcesByGroupKey,
|
||||||
onClearFilters
|
onClearFilters,
|
||||||
|
onResourceSelect
|
||||||
}: LauncherGroupListProps) {
|
}: LauncherGroupListProps) {
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -137,6 +140,7 @@ export function LauncherGroupList({
|
|||||||
config={config}
|
config={config}
|
||||||
initialResources={groupResources?.resources}
|
initialResources={groupResources?.resources}
|
||||||
initialResourcesPagination={groupResources?.pagination}
|
initialResourcesPagination={groupResources?.pagination}
|
||||||
|
onResourceSelect={onResourceSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type LauncherGroupSectionProps = {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
};
|
};
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
|
onResourceSelect: (resource: LauncherResource) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherGroupSection({
|
export function LauncherGroupSection({
|
||||||
@@ -49,7 +50,8 @@ export function LauncherGroupSection({
|
|||||||
config,
|
config,
|
||||||
initialResources,
|
initialResources,
|
||||||
initialResourcesPagination,
|
initialResourcesPagination,
|
||||||
defaultOpen = true
|
defaultOpen = true,
|
||||||
|
onResourceSelect
|
||||||
}: LauncherGroupSectionProps) {
|
}: LauncherGroupSectionProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -175,11 +177,13 @@ export function LauncherGroupSection({
|
|||||||
<LauncherResourceGrid
|
<LauncherResourceGrid
|
||||||
resources={resources}
|
resources={resources}
|
||||||
showLabels={config.showLabels}
|
showLabels={config.showLabels}
|
||||||
|
onResourceSelect={onResourceSelect}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LauncherResourceList
|
<LauncherResourceList
|
||||||
resources={resources}
|
resources={resources}
|
||||||
showLabels={config.showLabels}
|
showLabels={config.showLabels}
|
||||||
|
onResourceSelect={onResourceSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,26 +5,21 @@ import type { LauncherResource } from "@server/routers/launcher/types";
|
|||||||
import { LauncherLabelsRow } from "./LauncherLabelsRow";
|
import { LauncherLabelsRow } from "./LauncherLabelsRow";
|
||||||
import { LauncherResourceAccess } from "./LauncherResourceAccess";
|
import { LauncherResourceAccess } from "./LauncherResourceAccess";
|
||||||
import { LauncherResourceIcon } from "./LauncherResourceIcon";
|
import { LauncherResourceIcon } from "./LauncherResourceIcon";
|
||||||
import {
|
import { getLauncherResourceSelectProps } from "./useLauncherResourceAction";
|
||||||
getLauncherResourceClickProps,
|
|
||||||
useLauncherResourceAction
|
|
||||||
} from "./useLauncherResourceAction";
|
|
||||||
|
|
||||||
type LauncherResourceCardProps = {
|
type LauncherResourceCardProps = {
|
||||||
resource: LauncherResource;
|
resource: LauncherResource;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherResourceCard({
|
export function LauncherResourceCard({
|
||||||
resource,
|
resource,
|
||||||
showLabels
|
showLabels,
|
||||||
|
onSelect
|
||||||
}: LauncherResourceCardProps) {
|
}: LauncherResourceCardProps) {
|
||||||
const hasIcon = Boolean(resource.iconUrl);
|
const hasIcon = Boolean(resource.iconUrl);
|
||||||
const { handleAction, isClickable } = useLauncherResourceAction({
|
const clickProps = getLauncherResourceSelectProps(onSelect);
|
||||||
accessUrl: resource.accessUrl,
|
|
||||||
accessCopyValue: resource.accessCopyValue
|
|
||||||
});
|
|
||||||
const clickProps = getLauncherResourceClickProps(handleAction, isClickable);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { LauncherResourceCard } from "./LauncherResourceCard";
|
|||||||
type LauncherResourceGridProps = {
|
type LauncherResourceGridProps = {
|
||||||
resources: LauncherResource[];
|
resources: LauncherResource[];
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
onResourceSelect: (resource: LauncherResource) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherResourceGrid({
|
export function LauncherResourceGrid({
|
||||||
resources,
|
resources,
|
||||||
showLabels
|
showLabels,
|
||||||
|
onResourceSelect
|
||||||
}: LauncherResourceGridProps) {
|
}: LauncherResourceGridProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 [&>*]:min-w-0">
|
<div className="grid w-full grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 [&>*]:min-w-0">
|
||||||
@@ -19,6 +21,7 @@ export function LauncherResourceGrid({
|
|||||||
key={resource.launcherResourceKey}
|
key={resource.launcherResourceKey}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
|
onSelect={() => onResourceSelect(resource)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { LauncherResourceRow } from "./LauncherResourceRow";
|
|||||||
type LauncherResourceListProps = {
|
type LauncherResourceListProps = {
|
||||||
resources: LauncherResource[];
|
resources: LauncherResource[];
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
onResourceSelect: (resource: LauncherResource) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherResourceList({
|
export function LauncherResourceList({
|
||||||
resources,
|
resources,
|
||||||
showLabels
|
showLabels,
|
||||||
|
onResourceSelect
|
||||||
}: LauncherResourceListProps) {
|
}: LauncherResourceListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-md:overflow-x-auto max-md:overflow-y-hidden">
|
<div className="w-full max-md:overflow-x-auto max-md:overflow-y-hidden">
|
||||||
@@ -21,6 +23,7 @@ export function LauncherResourceList({
|
|||||||
resource={resource}
|
resource={resource}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
isLast={index === resources.length - 1}
|
isLast={index === resources.length - 1}
|
||||||
|
onSelect={() => onResourceSelect(resource)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SidePanel,
|
||||||
|
SidePanelBody,
|
||||||
|
SidePanelContent,
|
||||||
|
SidePanelDescription,
|
||||||
|
SidePanelFooter,
|
||||||
|
SidePanelHeader,
|
||||||
|
SidePanelTitle
|
||||||
|
} from "@app/components/SidePanel";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
|
||||||
|
import type { LauncherResource } from "@server/routers/launcher/types";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type LauncherResourcePanelProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
resource: LauncherResource | null;
|
||||||
|
orgId: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LauncherResourcePanel({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
resource,
|
||||||
|
orgId,
|
||||||
|
isAdmin
|
||||||
|
}: LauncherResourcePanelProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidePanel open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SidePanelContent>
|
||||||
|
<SidePanelHeader>
|
||||||
|
<SidePanelTitle>{resource?.name ?? ""}</SidePanelTitle>
|
||||||
|
<SidePanelDescription>
|
||||||
|
{t("resourceLauncherResourceDetailsDescription")}
|
||||||
|
</SidePanelDescription>
|
||||||
|
</SidePanelHeader>
|
||||||
|
<SidePanelBody />
|
||||||
|
<SidePanelFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{t("close")}
|
||||||
|
</Button>
|
||||||
|
{isAdmin && resource ? (
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link
|
||||||
|
href={getLauncherResourceAdminHref(
|
||||||
|
orgId,
|
||||||
|
resource
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t("resourceLauncherViewAsAdmin")}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</SidePanelFooter>
|
||||||
|
</SidePanelContent>
|
||||||
|
</SidePanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,28 +5,23 @@ import type { LauncherResource } from "@server/routers/launcher/types";
|
|||||||
import { LauncherLabelsRow } from "./LauncherLabelsRow";
|
import { LauncherLabelsRow } from "./LauncherLabelsRow";
|
||||||
import { LauncherResourceAccess } from "./LauncherResourceAccess";
|
import { LauncherResourceAccess } from "./LauncherResourceAccess";
|
||||||
import { LauncherResourceIcon } from "./LauncherResourceIcon";
|
import { LauncherResourceIcon } from "./LauncherResourceIcon";
|
||||||
import {
|
import { getLauncherResourceSelectProps } from "./useLauncherResourceAction";
|
||||||
getLauncherResourceClickProps,
|
|
||||||
useLauncherResourceAction
|
|
||||||
} from "./useLauncherResourceAction";
|
|
||||||
|
|
||||||
type LauncherResourceRowProps = {
|
type LauncherResourceRowProps = {
|
||||||
resource: LauncherResource;
|
resource: LauncherResource;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherResourceRow({
|
export function LauncherResourceRow({
|
||||||
resource,
|
resource,
|
||||||
showLabels,
|
showLabels,
|
||||||
isLast = false
|
isLast = false,
|
||||||
|
onSelect
|
||||||
}: LauncherResourceRowProps) {
|
}: LauncherResourceRowProps) {
|
||||||
const hasTags = showLabels && resource.labels.length > 0;
|
const hasTags = showLabels && resource.labels.length > 0;
|
||||||
const { handleAction, isClickable } = useLauncherResourceAction({
|
const clickProps = getLauncherResourceSelectProps(onSelect);
|
||||||
accessUrl: resource.accessUrl,
|
|
||||||
accessCopyValue: resource.accessCopyValue
|
|
||||||
});
|
|
||||||
const clickProps = getLauncherResourceClickProps(handleAction, isClickable);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useToast } from "@app/hooks/useToast";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import type {
|
import type {
|
||||||
LauncherGroup,
|
LauncherGroup,
|
||||||
|
LauncherResource,
|
||||||
LauncherViewConfig,
|
LauncherViewConfig,
|
||||||
LauncherViewRecord
|
LauncherViewRecord
|
||||||
} from "@server/routers/launcher/types";
|
} from "@server/routers/launcher/types";
|
||||||
@@ -55,6 +56,7 @@ import { LauncherGroupList } from "./LauncherGroupList";
|
|||||||
import { LauncherRefreshButton } from "./LauncherRefreshButton";
|
import { LauncherRefreshButton } from "./LauncherRefreshButton";
|
||||||
import { LauncherSettingsMenu } from "./LauncherSettingsMenu";
|
import { LauncherSettingsMenu } from "./LauncherSettingsMenu";
|
||||||
import { LauncherSortButton } from "./LauncherSortButton";
|
import { LauncherSortButton } from "./LauncherSortButton";
|
||||||
|
import { LauncherResourcePanel } from "./LauncherResourcePanel";
|
||||||
import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs";
|
import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
|
||||||
@@ -96,6 +98,8 @@ export default function ResourceLauncher({
|
|||||||
|
|
||||||
const [searchInputResetKey, setSearchInputResetKey] = useState(0);
|
const [searchInputResetKey, setSearchInputResetKey] = useState(0);
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
|
const [selectedResource, setSelectedResource] =
|
||||||
|
useState<LauncherResource | null>(null);
|
||||||
const [newViewName, setNewViewName] = useState("");
|
const [newViewName, setNewViewName] = useState("");
|
||||||
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
||||||
|
|
||||||
@@ -491,6 +495,19 @@ export default function ResourceLauncher({
|
|||||||
groupsPagination={groupsPagination}
|
groupsPagination={groupsPagination}
|
||||||
resourcesByGroupKey={resourcesByGroupKey}
|
resourcesByGroupKey={resourcesByGroupKey}
|
||||||
onClearFilters={handleClearFilters}
|
onClearFilters={handleClearFilters}
|
||||||
|
onResourceSelect={setSelectedResource}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LauncherResourcePanel
|
||||||
|
open={selectedResource != null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setSelectedResource(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
resource={selectedResource}
|
||||||
|
orgId={orgId}
|
||||||
|
isAdmin={isAdmin}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||||
|
|||||||
@@ -44,28 +44,67 @@ export function useLauncherResourceAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isLauncherResourceInteractiveTarget(
|
export function isLauncherResourceInteractiveTarget(
|
||||||
target: EventTarget | null
|
target: EventTarget | null,
|
||||||
|
container?: EventTarget | null
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!(target instanceof Element)) {
|
if (!(target instanceof Element)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Boolean(
|
const interactive = target.closest(
|
||||||
target.closest("a, button, [role='button'], input, textarea, select")
|
"a, button, [role='button'], input, textarea, select"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!interactive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container instanceof Element && interactive === container) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLauncherResourceClick(
|
function handleLauncherResourceClick(
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
handleAction: () => void
|
handleAction: () => void
|
||||||
) {
|
) {
|
||||||
if (isLauncherResourceInteractiveTarget(event.target)) {
|
if (
|
||||||
|
isLauncherResourceInteractiveTarget(event.target, event.currentTarget)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAction();
|
handleAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLauncherResourceSelectProps(onSelect: () => void) {
|
||||||
|
return {
|
||||||
|
onClick: (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
isLauncherResourceInteractiveTarget(
|
||||||
|
event.target,
|
||||||
|
event.currentTarget
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect();
|
||||||
|
},
|
||||||
|
className: "cursor-pointer",
|
||||||
|
role: "button" as const,
|
||||||
|
tabIndex: 0,
|
||||||
|
onKeyDown: (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getLauncherResourceClickProps(
|
export function getLauncherResourceClickProps(
|
||||||
handleAction: () => void,
|
handleAction: () => void,
|
||||||
isClickable: boolean
|
isClickable: boolean
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { LauncherResource } from "@server/routers/launcher/types";
|
||||||
|
|
||||||
|
export function getLauncherResourceAdminHref(
|
||||||
|
orgId: string,
|
||||||
|
resource: LauncherResource
|
||||||
|
): string {
|
||||||
|
if (resource.resourceType === "public") {
|
||||||
|
return `/${orgId}/settings/resources/public/${resource.niceId}/general`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qs = new URLSearchParams({ query: resource.niceId });
|
||||||
|
if (resource.site?.siteId != null) {
|
||||||
|
qs.set("siteId", String(resource.site.siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/${orgId}/settings/resources/private?${qs.toString()}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user