270 lines
9.3 KiB
TypeScript
270 lines
9.3 KiB
TypeScript
import { SideBarItem } from "./SidebarItem";
|
|
import { EntitySimple } from "~/utils/db/entities/entities.db.server";
|
|
import { TFunction, type i18n as I18n } from "i18next";
|
|
import { EntityGroupWithDetails } from "~/utils/db/entities/entityGroups.db.server";
|
|
import { AppConfiguration } from "~/utils/db/appConfiguration.db.server";
|
|
import IconSettings from "~/components/layouts/icons/IconSettings";
|
|
import IconDashboard from "~/components/layouts/icons/IconDashboard";
|
|
import IconAdmin from "~/components/layouts/icons/IconAdmin";
|
|
import IconNotifications from "~/components/layouts/icons/IconNotifications";
|
|
import IconEmails from "~/components/layouts/icons/IconEmails";
|
|
import IconKeys from "~/components/layouts/icons/IconKeys";
|
|
import IconWorkflows from "~/components/layouts/icons/IconWorkflows";
|
|
import CreditsIcon from "~/components/ui/icons/CreditsIcon";
|
|
import AddressBookIcon from "~/components/ui/icons/crm/AddressBookIcon";
|
|
import IconDocs from "~/components/layouts/icons/IconDocs";
|
|
import ViewListIcon from "~/components/ui/icons/ViewListIcon";
|
|
import { UI_ID_PATTERNS, UI_NAV_IDS, uiIdSegment } from "~/application/ui/uiRegistry";
|
|
import { appPath } from "~/utils/app/CanonicalUrlUtils";
|
|
import { absoluteAdminPath, type PublicDomainRoutingConfig } from "~/utils/app/CrossHostUrls";
|
|
import { getEntityPermission } from "~/utils/helpers/PermissionsHelper";
|
|
import { translateLabel } from "~/utils/i18n/translateLabel";
|
|
import { resolveConnectorNavState } from "~/utils/connector/connectorNavState";
|
|
import { WALKING_SKELETON_MODULE_KEY } from "~/modules/walkingSkeleton/constants";
|
|
|
|
type Props = {
|
|
t: TFunction;
|
|
i18n: I18n;
|
|
tenantId: string;
|
|
entities: EntitySimple[];
|
|
entityGroups: EntityGroupWithDetails[];
|
|
appConfiguration: AppConfiguration | null;
|
|
routing?: PublicDomainRoutingConfig;
|
|
productAccess?: Record<string, { allowed: boolean; reason?: string }>;
|
|
};
|
|
|
|
export const AppSidebar = ({ t, i18n, tenantId, entities, entityGroups, routing, productAccess }: Props): SideBarItem[] => {
|
|
const tenantPath = (path?: string) => appPath({ tenantSlug: tenantId, path, mode: "host-aware" });
|
|
const hasFeatureAccess = (featureKey: string) => productAccess?.[featureKey]?.allowed === true;
|
|
// R-P1c Konnektor-Demo: Walking-Skeleton-Eintrag zeigt die drei Gate-Zustaende (aktiv/gesperrt/ausgeblendet).
|
|
const walkingSkeletonDecision = productAccess?.[WALKING_SKELETON_MODULE_KEY];
|
|
const walkingSkeletonState = walkingSkeletonDecision ? resolveConnectorNavState(walkingSkeletonDecision) : "hidden";
|
|
|
|
const sectionItems: SideBarItem[] = [];
|
|
const entitiesItems: SideBarItem[] = [];
|
|
const productCoreItems: SideBarItem[] = [
|
|
...(hasFeatureAccess("crm-basic")
|
|
? [
|
|
{
|
|
id: UI_NAV_IDS.appCrm,
|
|
title: t("crm:title"),
|
|
path: tenantPath("crm"),
|
|
icon: <AddressBookIcon />,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("files")
|
|
? [
|
|
{
|
|
id: "ui.nav.app.files",
|
|
title: t("files.nav.title"),
|
|
path: tenantPath("files"),
|
|
icon: <IconDocs />,
|
|
permission: "files.read_own" as const,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("tasks")
|
|
? [
|
|
{
|
|
id: "ui.nav.app.tasks",
|
|
title: t("tasks.nav.title"),
|
|
path: tenantPath("tasks"),
|
|
icon: <ViewListIcon />,
|
|
permission: "tasks.read_own" as const,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("email-marketing")
|
|
? [
|
|
{
|
|
id: UI_NAV_IDS.appEmailMarketing,
|
|
title: t("emailMarketing:title"),
|
|
path: tenantPath("email-marketing"),
|
|
icon: <IconEmails />,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("workflow-engine")
|
|
? [
|
|
{
|
|
id: UI_NAV_IDS.appWorkflowEngine,
|
|
title: t("workflows.title"),
|
|
path: tenantPath("workflow-engine"),
|
|
icon: <IconWorkflows />,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("api")
|
|
? [
|
|
{
|
|
id: UI_NAV_IDS.appApi,
|
|
title: t("models.apiKey.plural"),
|
|
path: tenantPath("settings/api"),
|
|
icon: <IconKeys />,
|
|
},
|
|
]
|
|
: []),
|
|
...(hasFeatureAccess("ai-credits")
|
|
? [
|
|
{
|
|
id: UI_NAV_IDS.appCredits,
|
|
title: t("models.credit.plural"),
|
|
path: tenantPath("settings/credits"),
|
|
icon: <CreditsIcon />,
|
|
},
|
|
]
|
|
: []),
|
|
...(walkingSkeletonState !== "hidden"
|
|
? [
|
|
{
|
|
id: "ui.nav.app.walkingSkeleton",
|
|
title: t("walkingSkeleton.nav"),
|
|
path: tenantPath("walking-skeleton"),
|
|
icon: <IconDashboard />,
|
|
disabled: walkingSkeletonState === "locked",
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
|
|
entityGroups.forEach((group) => {
|
|
const item: SideBarItem = {
|
|
id: `ui.nav.app.entityGroup.${uiIdSegment(group.slug)}`,
|
|
uiIdPattern: UI_ID_PATTERNS.appEntityGroup,
|
|
uiIdParams: { groupSlug: group.slug },
|
|
title: translateLabel(t, i18n, group.title),
|
|
// icon: group.icon,
|
|
path: tenantPath(`g/${group.slug}`),
|
|
icon: group.icon,
|
|
// isActive: true,
|
|
};
|
|
if (group.collapsible) {
|
|
item.isCollapsible = true;
|
|
item.items = [
|
|
{
|
|
id: `ui.nav.app.entityGroup.${uiIdSegment(group.slug)}.overview`,
|
|
title: t("shared.overview"),
|
|
path: tenantPath(`g/${group.slug}`),
|
|
},
|
|
...group.entities.map(({ entity }) => {
|
|
return {
|
|
id: `ui.nav.app.entityGroup.${uiIdSegment(group.slug)}.entity.${uiIdSegment(entity.slug)}`,
|
|
uiIdPattern: UI_ID_PATTERNS.appEntityGroupEntity,
|
|
uiIdParams: { groupSlug: group.slug, entityName: entity.name ?? entity.slug },
|
|
title: translateLabel(t, i18n, entity.titlePlural),
|
|
// icon: entity.icon,
|
|
path: tenantPath(`g/${group.slug}/${entity.slug}`),
|
|
permission: getEntityPermission(entity, "view"),
|
|
// entityIcon: entity.icon,
|
|
};
|
|
}),
|
|
];
|
|
}
|
|
if (group.section) {
|
|
const sectionTitle = translateLabel(t, i18n, group.section);
|
|
const section = sectionItems.find((f) => f.title === sectionTitle);
|
|
if (section) {
|
|
section.items = [...(section.items ?? []), item];
|
|
} else {
|
|
sectionItems.push({
|
|
id: `ui.nav.app.section.${uiIdSegment(group.section)}`,
|
|
uiIdPattern: UI_ID_PATTERNS.appSection,
|
|
uiIdParams: { sectionName: group.section },
|
|
title: sectionTitle,
|
|
path: ``,
|
|
items: [item],
|
|
});
|
|
}
|
|
} else {
|
|
entitiesItems.push(item);
|
|
}
|
|
});
|
|
|
|
entities
|
|
.filter((f) => f.showInSidebar)
|
|
.forEach((entity) => {
|
|
entitiesItems.push({
|
|
id: `ui.nav.app.entity.${uiIdSegment(entity.slug)}`,
|
|
uiIdPattern: UI_ID_PATTERNS.appEntity,
|
|
uiIdParams: { entityName: entity.name ?? entity.slug, entitySlug: entity.slug },
|
|
title: translateLabel(t, i18n, entity.titlePlural),
|
|
icon: entity.icon,
|
|
// tenantUserTypes: [TenantUserType.OWNER, TenantUserType.ADMIN],
|
|
path: tenantPath(entity.slug),
|
|
permission: getEntityPermission(entity, "view"),
|
|
// side: isDebugMode && SideText({ text: modelProperties.length > 0 ? "DB Model" : "Custom" }),
|
|
});
|
|
});
|
|
|
|
const appItem: SideBarItem = {
|
|
id: "ui.nav.app.primary",
|
|
title: "",
|
|
path: "",
|
|
items: [
|
|
{
|
|
id: UI_NAV_IDS.appDashboard,
|
|
title: t("app.sidebar.dashboard"),
|
|
path: tenantPath("dashboard"),
|
|
icon: <IconDashboard />,
|
|
// tenantUserTypes: [TenantUserType.OWNER, TenantUserType.ADMIN, TenantUserType.MEMBER],
|
|
},
|
|
{
|
|
id: UI_NAV_IDS.appNotifications,
|
|
title: t("app.sidebar.notifications"),
|
|
path: tenantPath("notifications"),
|
|
icon: <IconNotifications />,
|
|
},
|
|
...productCoreItems,
|
|
...entitiesItems,
|
|
// {
|
|
// title: t("models.email.plural"),
|
|
// path: `${currentTenantUrl}/emails`,
|
|
// icon: SvgIcon.EMAILS,
|
|
// },
|
|
],
|
|
};
|
|
|
|
return [
|
|
appItem,
|
|
...sectionItems,
|
|
{
|
|
id: "ui.nav.app.secondary",
|
|
title: "",
|
|
path: "",
|
|
isSecondary: true,
|
|
items: [
|
|
{
|
|
id: UI_NAV_IDS.appSettings,
|
|
title: t("app.sidebar.settings"),
|
|
icon: <IconSettings />,
|
|
// tenantUserTypes: [TenantUserType.OWNER, TenantUserType.ADMIN],
|
|
path: tenantPath("settings"),
|
|
redirectTo: tenantPath("settings/profile"),
|
|
},
|
|
// {
|
|
// title: t("app.sidebar.logs"),
|
|
// icon: SvgIcon.LOGS,
|
|
// tenantUserTypes: [TenantUserType.OWNER, TenantUserType.ADMIN],
|
|
// path: `${currentTenantUrl}/logs`,
|
|
// },
|
|
{
|
|
id: UI_NAV_IDS.appSwitchToAdmin,
|
|
title: t("admin.switchToAdmin"),
|
|
path: absoluteAdminPath(routing, "dashboard"),
|
|
icon: <IconAdmin />,
|
|
adminOnly: true,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
};
|
|
|
|
// function SideText({ text }: { text: string }) {
|
|
// return (
|
|
// <div className="inline-flex justify-center space-x-1 items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-900 italic truncate w-20">
|
|
// <div className="truncate">{text}</div>
|
|
// </div>
|
|
// );
|
|
// }
|