305 lines
10 KiB
TypeScript
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;
|
|
}
|