From 376dd465b3bf8a6c545ca01ede19cc477c7644c7 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 30 Jun 2026 21:49:08 -0400 Subject: [PATCH] show no site category --- messages/en-US.json | 1 + .../launcher/launcherResourceAccess.ts | 114 +++++++++++++++++- server/routers/launcher/types.ts | 1 + .../LauncherGroupSection.tsx | 10 +- 4 files changed, 121 insertions(+), 5 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 289399e5c..be9fe0d3d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3572,6 +3572,7 @@ "resourceLauncherDeleteView": "Delete View", "resourceLauncherViewAsAdmin": "View as Admin", "resourceLauncherUnlabeled": "Unlabeled", + "resourceLauncherNoSite": "No Site", "resourceLauncherNoResourcesInGroup": "No resources in this group", "resourceLauncherEmptyStateTitle": "No Resources Available", "resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.", diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts index 95b2b16e1..47e3529fe 100644 --- a/server/routers/launcher/launcherResourceAccess.ts +++ b/server/routers/launcher/launcherResourceAccess.ts @@ -27,6 +27,7 @@ import { countDistinct, eq, inArray, + isNull, like, or, sql @@ -38,6 +39,7 @@ import { formatSiteResourceAccess } from "./formatLauncherAccess"; import { + LAUNCHER_NO_SITE_GROUP_KEY, LAUNCHER_UNLABELED_GROUP_KEY, type LauncherGroup, type LauncherLabel, @@ -508,6 +510,83 @@ async function listSiteGroups( } } + let noSiteCount = 0; + + if (accessible.resourceIds.length > 0 && siteFilterIds.length === 0) { + const noSitePublicConditions = [ + inArray(resources.resourceId, accessible.resourceIds), + eq(resources.orgId, orgId), + eq(resources.enabled, true) + ]; + if (searchPublic) { + noSitePublicConditions.push(searchPublic); + } + + let noSitePublicQuery = db + .select({ + itemCount: countDistinct(resources.resourceId) + }) + .from(resources) + .leftJoin(targets, eq(targets.resourceId, resources.resourceId)); + + if (labelFilterIds.length > 0) { + noSitePublicQuery = noSitePublicQuery.innerJoin( + resourceLabels, + eq(resourceLabels.resourceId, resources.resourceId) + ); + noSitePublicConditions.push( + inArray(resourceLabels.labelId, labelFilterIds) + ); + } + + const [noSitePublicRow] = await noSitePublicQuery.where( + and(...noSitePublicConditions, isNull(targets.targetId)) + ); + + noSiteCount += Number(noSitePublicRow?.itemCount ?? 0); + } + + if (accessible.siteResourceIds.length > 0 && siteFilterIds.length === 0) { + const noSiteSiteConditions = [ + inArray(siteResources.siteResourceId, accessible.siteResourceIds), + eq(siteResources.orgId, orgId), + eq(siteResources.enabled, true) + ]; + if (searchSite) { + noSiteSiteConditions.push(searchSite); + } + + let noSiteSiteQuery = db + .select({ + itemCount: countDistinct(siteResources.siteResourceId) + }) + .from(siteResources) + .leftJoin( + siteNetworks, + eq(siteResources.networkId, siteNetworks.networkId) + ) + .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId)); + + if (labelFilterIds.length > 0) { + noSiteSiteQuery = noSiteSiteQuery.innerJoin( + siteResourceLabels, + eq( + siteResourceLabels.siteResourceId, + siteResources.siteResourceId + ) + ); + noSiteSiteConditions.push( + inArray(siteResourceLabels.labelId, labelFilterIds) + ); + } + + const [noSiteSiteRow] = await noSiteSiteQuery.where( + and(...noSiteSiteConditions, isNull(sites.siteId)) + ); + + noSiteCount += Number(noSiteSiteRow?.itemCount ?? 0); + } + let groups: LauncherGroup[] = Array.from(siteCountMap.values()).map( (row) => ({ groupKey: String(row.siteId), @@ -519,6 +598,15 @@ async function listSiteGroups( }) ); + if (noSiteCount > 0 && siteFilterIds.length === 0) { + groups.push({ + groupKey: LAUNCHER_NO_SITE_GROUP_KEY, + name: "No Site", + groupType: "site", + itemCount: noSiteCount + }); + } + groups.sort((a, b) => { const cmp = a.name.localeCompare(b.name, undefined, { sensitivity: "base" @@ -923,6 +1011,20 @@ async function mapSiteResources( return result; } +function filterResourcesBySite( + items: LauncherResource[], + groupKey: string +): LauncherResource[] { + if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) { + return items.filter((item) => !item.site); + } + const siteId = Number.parseInt(groupKey, 10); + if (!Number.isFinite(siteId)) { + return items; + } + return items.filter((item) => item.site?.siteId === siteId); +} + function filterResourcesByLabel( items: LauncherResource[], groupKey: string @@ -1052,10 +1154,14 @@ export async function listLauncherResourcesForUser( filteredSiteResourceIds ); - const siteIdFilter = - query.groupBy === "site" + const parsedSiteId = + query.groupBy === "site" && + query.groupKey !== LAUNCHER_NO_SITE_GROUP_KEY ? Number.parseInt(query.groupKey, 10) - : undefined; + : Number.NaN; + const siteIdFilter = Number.isFinite(parsedSiteId) + ? parsedSiteId + : undefined; const [publicItems, siteItems] = await Promise.all([ mapPublicResources( @@ -1077,6 +1183,8 @@ export async function listLauncherResourcesForUser( if (query.groupBy === "label") { items = filterResourcesByLabel(items, query.groupKey); + } else if (query.groupBy === "site") { + items = filterResourcesBySite(items, query.groupKey); } items = sortLauncherResources(items, query.order); diff --git a/server/routers/launcher/types.ts b/server/routers/launcher/types.ts index 6774a1549..eba5e8530 100644 --- a/server/routers/launcher/types.ts +++ b/server/routers/launcher/types.ts @@ -1,6 +1,7 @@ import { z } from "zod"; export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled"; +export const LAUNCHER_NO_SITE_GROUP_KEY = "no-site"; export const launcherViewConfigSchema = z.object({ groupBy: z.enum(["site", "label"]).default("site"), diff --git a/src/components/resource-launcher/LauncherGroupSection.tsx b/src/components/resource-launcher/LauncherGroupSection.tsx index 8c32d9074..7b80c4a0f 100644 --- a/src/components/resource-launcher/LauncherGroupSection.tsx +++ b/src/components/resource-launcher/LauncherGroupSection.tsx @@ -16,6 +16,10 @@ import type { LauncherResource, LauncherViewConfig } from "@server/routers/launcher/types"; +import { + LAUNCHER_NO_SITE_GROUP_KEY, + LAUNCHER_UNLABELED_GROUP_KEY +} from "@server/routers/launcher/types"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Loader2 } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -140,9 +144,11 @@ export function LauncherGroupSection({ }, [fetchNextPage, hasNextPage, isFetchingNextPage, isOpen]); const groupTitle = - group.groupKey === "unlabeled" + group.groupKey === LAUNCHER_UNLABELED_GROUP_KEY ? t("resourceLauncherUnlabeled") - : group.name; + : group.groupKey === LAUNCHER_NO_SITE_GROUP_KEY + ? t("resourceLauncherNoSite") + : group.name; return (