Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/services/command-registry/commandRegistry.ts
T

305 lines
10 KiB
TypeScript

import type { ReactNode } from "react";
import { TFunction, i18n } from "i18next";
import { NavigateFunction } from "react-router";
import { AdminSidebar } from "~/application/sidebar/AdminSidebar";
import { SideBarItem } from "~/application/sidebar/SidebarItem";
import { getUserHasPermission } from "~/utils/helpers/PermissionsHelper";
import { AppLoaderData } from "~/utils/data/useAppData";
import { AppRootData } from "~/utils/data/useRootData";
import { getDocCommands } from "~/utils/services/docsService";
import { appPath } from "~/utils/app/CanonicalUrlUtils";
import { absoluteAdminPath, absoluteAppRootPath, isAbsoluteHttpUrl, type PublicDomainRoutingConfig } from "~/utils/app/CrossHostUrls";
import { filterCommandActions } from "~/services/search/commandSearch";
import { translateLabel } from "~/utils/i18n/translateLabel";
// Registry marker: services/command-registry paths are app-relative for package verification.
export type CommandRegistryLayout = "app" | "admin" | "docs";
export type CommandActionScope = {
layout: CommandRegistryLayout;
tenantSlug?: string;
platform?: boolean;
adminOnly?: boolean;
featureFlag?: string;
permission?: string;
};
export type CommandAction = {
id: string;
name: string;
subtitle?: string;
keywords?: string;
shortcut?: string[];
icon?: ReactNode;
perform?: (currentAction: CommandAction) => void;
};
export type ScopedCommandAction = CommandAction & {
scope: CommandActionScope;
};
export type CommandRegistryContext = {
layout: CommandRegistryLayout;
t: TFunction;
i18n?: Pick<i18n, "exists">;
navigate: NavigateFunction;
submitLogout?: () => void;
appData?: AppLoaderData;
rootData?: AppRootData;
};
export function getCommandRegistryActions(context: CommandRegistryContext): ScopedCommandAction[] {
const actions = getUnfilteredCommandRegistryActions(context);
return filterCommandActions({
actions,
query: "",
layout: context.layout,
tenantSlug: context.appData?.currentTenant?.slug,
appData: context.appData,
rootData: context.rootData,
});
}
function getUnfilteredCommandRegistryActions(context: CommandRegistryContext): ScopedCommandAction[] {
if (context.layout === "app") {
return getAppCommandActions(context);
}
if (context.layout === "admin") {
return getAdminCommandActions(context);
}
return getDocsCommandActions(context);
}
function getAppCommandActions({ t, i18n, navigate, submitLogout, appData, rootData }: CommandRegistryContext): ScopedCommandAction[] {
if (!appData || !rootData) {
return [];
}
const tenantSlug = appData.currentTenant.slug;
const tenantPath = (path?: string) => appPath({ tenantSlug, path, mode: "host-aware" });
const tenantScope = (metadata?: Omit<CommandActionScope, "layout" | "tenantSlug">): CommandActionScope => ({ layout: "app", tenantSlug, ...metadata });
const actions: ScopedCommandAction[] = [
{
id: "dashboard",
shortcut: [],
name: t("app.sidebar.dashboard"),
keywords: "",
scope: tenantScope(),
perform: () => navigate(tenantPath("dashboard")),
},
];
appData.entities
.filter((entity) => entity.showInSidebar)
.forEach((entity) => {
const permission = typeof entity.name === "string" && entity.name.trim() ? `entity.${entity.name}.view` : undefined;
actions.push({
id: "entity-" + entity.name,
name: translateLabel(t, i18n, entity.titlePlural),
scope: tenantScope({ permission }),
perform: () => navigate(tenantPath(entity.slug)),
});
});
actions.push({
id: "profile",
shortcut: [],
name: t("app.commands.profile.title"),
subtitle: t("app.sidebar.settings"),
keywords: t("app.commands.profile.description"),
scope: tenantScope(),
perform: () => navigate(tenantPath("settings/profile")),
});
pushPermissionAction(actions, appData, "app.settings.members.view", {
id: "members",
name: t("app.navbar.members"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ permission: "app.settings.members.view" }),
perform: () => navigate(tenantPath("settings/members")),
});
pushPermissionAction(actions, appData, "app.settings.subscription.view", {
id: "subscription",
name: t("app.navbar.subscription"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ permission: "app.settings.subscription.view" }),
perform: () => navigate(tenantPath("settings/subscription")),
});
pushPermissionAction(actions, appData, "app.settings.account.view", {
id: "account",
name: t("app.navbar.tenant"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ permission: "app.settings.account.view" }),
perform: () => navigate(tenantPath("settings/account")),
});
if (rootData.appConfiguration.app.features.tenantApiKeys) {
pushPermissionAction(actions, appData, "app.settings.apiKeys.view", {
id: "apiKeys",
name: t("models.apiKey.plural"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ featureFlag: "tenantApiKeys", permission: "app.settings.apiKeys.view" }),
perform: () => navigate(tenantPath("settings/api")),
});
}
if (getUserHasPermission(appData, "app.settings.auditTrails.view")) {
actions.push({
id: "auditTrails",
name: t("models.log.plural"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ permission: "app.settings.auditTrails.view" }),
perform: () => navigate(tenantPath("settings/logs")),
});
actions.push({
id: "events",
name: t("models.event.plural"),
shortcut: [],
keywords: "",
subtitle: t("app.sidebar.settings"),
scope: tenantScope({ permission: "app.settings.auditTrails.view" }),
perform: () => navigate(tenantPath("settings/logs/events")),
});
}
if (appData.myTenants.length > 0) {
actions.push({
id: "switch account",
shortcut: [],
name: t("app.commands.tenants.switch"),
keywords: t("app.commands.tenants.description"),
scope: { layout: "app", platform: true },
perform: () => navigateOrAssign(absoluteAppRootPath(rootData.routing), navigate),
});
}
actions.push({
id: "create account",
shortcut: [],
name: t("app.commands.tenants.create"),
keywords: "",
scope: { layout: "app", platform: true },
perform: () => navigateOrAssign(absolutePublicAppPath(rootData.routing, "new-account"), navigate),
});
if (appData.user?.admin) {
actions.push({
id: "switch to admin",
shortcut: [],
name: t("admin.switchToAdmin"),
keywords: "",
scope: { layout: "app", platform: true, adminOnly: true },
perform: () => navigateOrAssign(absoluteAdminPath(rootData.routing, "dashboard"), navigate),
});
}
actions.push({
id: "logout",
shortcut: [],
name: t("app.commands.profile.logout"),
scope: { layout: "app", platform: true },
perform: () => submitLogout?.(),
});
return actions;
}
function pushPermissionAction(actions: ScopedCommandAction[], appData: AppLoaderData, permission: string, action: ScopedCommandAction) {
if (getUserHasPermission(appData, permission as never)) {
actions.push(action);
}
}
function getAdminCommandActions({ t, navigate, submitLogout, rootData }: CommandRegistryContext): ScopedCommandAction[] {
if (!rootData) {
return [];
}
const actions = getSidebarCommandActions(AdminSidebar({ t, appConfiguration: rootData.appConfiguration, routing: rootData.routing }), "admin").map(
(action) => ({
...action,
perform(currentAction: CommandAction) {
navigateOrAssign(currentAction.id, navigate);
},
})
);
actions.push({
id: "create account",
shortcut: [],
name: t("app.commands.tenants.create"),
keywords: "",
scope: { layout: "admin", platform: true },
perform: () => navigateOrAssign(absolutePublicAppPath(rootData.routing, "new-account"), navigate),
});
actions.push({
id: "logout",
shortcut: [],
name: t("app.commands.profile.logout"),
scope: { layout: "admin", platform: true },
perform: () => submitLogout?.(),
});
return actions;
}
function navigateOrAssign(target: string, navigate: NavigateFunction) {
if (isAbsoluteHttpUrl(target) && typeof window !== "undefined") {
window.location.assign(target);
return;
}
navigate(target);
}
function absolutePublicAppPath(routing: PublicDomainRoutingConfig | undefined, path: string) {
const appRoot = absoluteAppRootPath(routing);
const cleanPath = path.replace(/^\/+/, "");
if (isAbsoluteHttpUrl(appRoot)) {
return new URL(cleanPath, appRoot).toString();
}
return `/${cleanPath}`;
}
function getDocsCommandActions({ navigate }: CommandRegistryContext): ScopedCommandAction[] {
return getDocCommands().map((action) => ({
...action,
scope: { layout: "docs", platform: true },
perform(currentAction) {
navigate(currentAction.id);
},
}));
}
export function getSidebarCommandActions(items: SideBarItem[], layout: CommandRegistryLayout): ScopedCommandAction[] {
const commands: ScopedCommandAction[] = [];
items.forEach((item) => commands.push(...getCommandsFromSidebarItem(item, layout, [], [])));
return commands;
}
function getCommandsFromSidebarItem(item: SideBarItem, layout: CommandRegistryLayout, commands: ScopedCommandAction[], parent: string[]) {
if (item.hidden || item.disabled) {
return commands;
}
const isExpandableOnlyParent = item.isCollapsible && item.items && item.items.length > 0;
if (item.path && item.title && !isExpandableOnlyParent) {
let description = item.description ?? "";
const parentTitles = parent.filter((value) => value);
if (parentTitles.length > 0) {
description = parentTitles.join(" / ");
}
commands.push({
id: item.path,
name: item.title,
shortcut: [],
subtitle: description,
scope: { layout, platform: true, adminOnly: layout === "admin", permission: item.permission },
});
}
item.items?.forEach((subItem) => getCommandsFromSidebarItem(subItem, layout, commands, [...parent, item.title]));
return commands;
}