void;
};
export function LauncherResourceCard({
resource,
- showLabels
+ showLabels,
+ onSelect
}: LauncherResourceCardProps) {
const hasIcon = Boolean(resource.iconUrl);
- const { handleAction, isClickable } = useLauncherResourceAction({
- accessUrl: resource.accessUrl,
- accessCopyValue: resource.accessCopyValue
- });
- const clickProps = getLauncherResourceClickProps(handleAction, isClickable);
+ const clickProps = getLauncherResourceSelectProps(onSelect);
return (
void;
};
export function LauncherResourceGrid({
resources,
- showLabels
+ showLabels,
+ onResourceSelect
}: LauncherResourceGridProps) {
return (
@@ -19,6 +21,7 @@ export function LauncherResourceGrid({
key={resource.launcherResourceKey}
resource={resource}
showLabels={showLabels}
+ onSelect={() => onResourceSelect(resource)}
/>
))}
diff --git a/src/components/resource-launcher/LauncherResourceList.tsx b/src/components/resource-launcher/LauncherResourceList.tsx
index d95b58adf..e64e2b7b0 100644
--- a/src/components/resource-launcher/LauncherResourceList.tsx
+++ b/src/components/resource-launcher/LauncherResourceList.tsx
@@ -6,11 +6,13 @@ import { LauncherResourceRow } from "./LauncherResourceRow";
type LauncherResourceListProps = {
resources: LauncherResource[];
showLabels: boolean;
+ onResourceSelect: (resource: LauncherResource) => void;
};
export function LauncherResourceList({
resources,
- showLabels
+ showLabels,
+ onResourceSelect
}: LauncherResourceListProps) {
return (
@@ -21,6 +23,7 @@ export function LauncherResourceList({
resource={resource}
showLabels={showLabels}
isLast={index === resources.length - 1}
+ onSelect={() => onResourceSelect(resource)}
/>
))}
diff --git a/src/components/resource-launcher/LauncherResourcePanel.tsx b/src/components/resource-launcher/LauncherResourcePanel.tsx
new file mode 100644
index 000000000..1bb61bd9e
--- /dev/null
+++ b/src/components/resource-launcher/LauncherResourcePanel.tsx
@@ -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 (
+
+
+
+ {resource?.name ?? ""}
+
+ {t("resourceLauncherResourceDetailsDescription")}
+
+
+
+
+
+ {isAdmin && resource ? (
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/src/components/resource-launcher/LauncherResourceRow.tsx b/src/components/resource-launcher/LauncherResourceRow.tsx
index d94823b27..34558de51 100644
--- a/src/components/resource-launcher/LauncherResourceRow.tsx
+++ b/src/components/resource-launcher/LauncherResourceRow.tsx
@@ -5,28 +5,23 @@ import type { LauncherResource } from "@server/routers/launcher/types";
import { LauncherLabelsRow } from "./LauncherLabelsRow";
import { LauncherResourceAccess } from "./LauncherResourceAccess";
import { LauncherResourceIcon } from "./LauncherResourceIcon";
-import {
- getLauncherResourceClickProps,
- useLauncherResourceAction
-} from "./useLauncherResourceAction";
+import { getLauncherResourceSelectProps } from "./useLauncherResourceAction";
type LauncherResourceRowProps = {
resource: LauncherResource;
showLabels: boolean;
isLast?: boolean;
+ onSelect: () => void;
};
export function LauncherResourceRow({
resource,
showLabels,
- isLast = false
+ isLast = false,
+ onSelect
}: LauncherResourceRowProps) {
const hasTags = showLabels && resource.labels.length > 0;
- const { handleAction, isClickable } = useLauncherResourceAction({
- accessUrl: resource.accessUrl,
- accessCopyValue: resource.accessCopyValue
- });
- const clickProps = getLauncherResourceClickProps(handleAction, isClickable);
+ const clickProps = getLauncherResourceSelectProps(onSelect);
return (
(null);
const [newViewName, setNewViewName] = useState("");
const [saveOrgWide, setSaveOrgWide] = useState(false);
@@ -491,6 +495,19 @@ export default function ResourceLauncher({
groupsPagination={groupsPagination}
resourcesByGroupKey={resourcesByGroupKey}
onClearFilters={handleClearFilters}
+ onResourceSelect={setSelectedResource}
+ />
+
+ {
+ if (!open) {
+ setSelectedResource(null);
+ }
+ }}
+ resource={selectedResource}
+ orgId={orgId}
+ isAdmin={isAdmin}
/>
diff --git a/src/components/resource-launcher/useLauncherResourceAction.ts b/src/components/resource-launcher/useLauncherResourceAction.ts
index 427ee64bf..4c7d081dd 100644
--- a/src/components/resource-launcher/useLauncherResourceAction.ts
+++ b/src/components/resource-launcher/useLauncherResourceAction.ts
@@ -44,28 +44,67 @@ export function useLauncherResourceAction({
}
export function isLauncherResourceInteractiveTarget(
- target: EventTarget | null
+ target: EventTarget | null,
+ container?: EventTarget | null
): boolean {
if (!(target instanceof Element)) {
return false;
}
- return Boolean(
- target.closest("a, button, [role='button'], input, textarea, select")
+ const interactive = target.closest(
+ "a, button, [role='button'], input, textarea, select"
);
+
+ if (!interactive) {
+ return false;
+ }
+
+ if (container instanceof Element && interactive === container) {
+ return false;
+ }
+
+ return true;
}
function handleLauncherResourceClick(
event: MouseEvent,
handleAction: () => void
) {
- if (isLauncherResourceInteractiveTarget(event.target)) {
+ if (
+ isLauncherResourceInteractiveTarget(event.target, event.currentTarget)
+ ) {
return;
}
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(
handleAction: () => void,
isClickable: boolean
diff --git a/src/lib/launcherResourceAdminHref.ts b/src/lib/launcherResourceAdminHref.ts
new file mode 100644
index 000000000..db7da151e
--- /dev/null
+++ b/src/lib/launcherResourceAdminHref.ts
@@ -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()}`;
+}