From 5e6a14da280485586bd4c79397601b9147924f4e Mon Sep 17 00:00:00 2001 From: Jan Cizmar Date: Fri, 29 Nov 2024 18:23:53 +0100 Subject: [PATCH] chore: Project menu with plugin --- webapp/eePlugin/eePlugin.ee.tsx | 25 +- .../security/UserMenu/UserMenuItems.tsx} | 4 +- .../UserMenu/UserMissingAvatarMenu.tsx | 2 +- .../UserMenu/UserPresentAvatarMenu.tsx | 2 +- .../UserMenu/UserUnverifiedEmailMenu.tsx | 2 +- webapp/src/plugin/EePluginType.ts | 4 +- .../projects/projectMenu/ProjectMenu.tsx | 295 +++++++++--------- .../projects/projectMenu/SideMenuItem.tsx | 18 +- 8 files changed, 191 insertions(+), 161 deletions(-) rename webapp/src/{hooks/useUserMenuItems.tsx => component/security/UserMenu/UserMenuItems.tsx} (96%) diff --git a/webapp/eePlugin/eePlugin.ee.tsx b/webapp/eePlugin/eePlugin.ee.tsx index 5a37624585..e78577034e 100644 --- a/webapp/eePlugin/eePlugin.ee.tsx +++ b/webapp/eePlugin/eePlugin.ee.tsx @@ -41,8 +41,9 @@ import { GlobalLimitPopover } from '../src/ee/billing/limitPopover/GlobalLimitPo import { addDeveloperViewItems } from '../src/views/projects/developer/developerViewItems'; import { StorageList } from '../src/ee/developer/storage/StorageList'; import { WebhookList } from '../src/ee/developer/webhook/WebhookList'; -import { addUserMenuItems } from '../src/hooks/useUserMenuItems'; import { Badge, Box, MenuItem } from '@mui/material'; +import { addUserMenuItems } from '../src/component/security/UserMenu/UserMenuItems'; +import { addProjectMenuItems } from '../src/views/projects/projectMenu/ProjectMenu'; export const eePlugin: EePluginType = { ee: { @@ -271,6 +272,28 @@ export const eePlugin: EePluginType = { { position: 'start' } ); }, + useAddProjectMenuItems: () => { + const { t } = useTranslate(); + + return addProjectMenuItems( + [ + { + id: 'tasks', + condition: ({ satisfiesPermission }) => + satisfiesPermission('tasks.view'), + link: LINKS.PROJECT_TASKS, + icon: ClipboardCheck, + text: t('project_menu_tasks'), + dataCy: 'project-menu-item-tasks', + matchAsPrefix: true, + }, + ], + { + position: 'after', + value: 'translations', + } + ); + }, }, }; diff --git a/webapp/src/hooks/useUserMenuItems.tsx b/webapp/src/component/security/UserMenu/UserMenuItems.tsx similarity index 96% rename from webapp/src/hooks/useUserMenuItems.tsx rename to webapp/src/component/security/UserMenu/UserMenuItems.tsx index 1dcb63bf15..b4db02f3c6 100644 --- a/webapp/src/hooks/useUserMenuItems.tsx +++ b/webapp/src/component/security/UserMenu/UserMenuItems.tsx @@ -1,7 +1,7 @@ import { useTranslate } from '@tolgee/react'; import { Link, useLocation } from 'react-router-dom'; -import { LINKS } from '../constants/links'; +import { LINKS } from 'tg.constants/links'; import { useConfig, useIsEmailVerified, @@ -10,7 +10,7 @@ import { import { MenuItem } from '@mui/material'; import React, { FC } from 'react'; import { createAdder } from 'tg.fixtures/pluginAdder'; -import { getEe } from '../plugin/getEe'; +import { getEe } from '../../../plugin/getEe'; export const UserMenuItems: FC<{ onClose: () => void }> = ({ onClose }) => { const location = useLocation(); diff --git a/webapp/src/component/security/UserMenu/UserMissingAvatarMenu.tsx b/webapp/src/component/security/UserMenu/UserMissingAvatarMenu.tsx index b37c6f6821..962211467a 100644 --- a/webapp/src/component/security/UserMenu/UserMissingAvatarMenu.tsx +++ b/webapp/src/component/security/UserMenu/UserMissingAvatarMenu.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import { IconButton, Popover, styled } from '@mui/material'; -import { UserMenuItems } from 'tg.hooks/useUserMenuItems'; import { UserAvatar } from 'tg.component/common/avatar/UserAvatar'; import { ThemeItem } from './ThemeItem'; import { LanguageItem } from './LanguageItem'; +import { UserMenuItems } from './UserMenuItems'; const StyledIconButton = styled(IconButton)` width: 40px; diff --git a/webapp/src/component/security/UserMenu/UserPresentAvatarMenu.tsx b/webapp/src/component/security/UserMenu/UserPresentAvatarMenu.tsx index 27a050e570..80ca2e270c 100644 --- a/webapp/src/component/security/UserMenu/UserPresentAvatarMenu.tsx +++ b/webapp/src/component/security/UserMenu/UserPresentAvatarMenu.tsx @@ -4,7 +4,6 @@ import { T, useTranslate } from '@tolgee/react'; import { Link, useHistory, useLocation } from 'react-router-dom'; import { usePreferredOrganization, useUser } from 'tg.globalContext/helpers'; -import { UserMenuItems } from 'tg.hooks/useUserMenuItems'; import { UserAvatar } from 'tg.component/common/avatar/UserAvatar'; import { LINKS, PARAMS } from 'tg.constants/links'; import { components } from 'tg.service/apiSchema.generated'; @@ -15,6 +14,7 @@ import { ThemeItem } from './ThemeItem'; import { LanguageItem } from './LanguageItem'; import { useGlobalActions } from 'tg.globalContext/GlobalContext'; import { getEe } from '../../../plugin/getEe'; +import { UserMenuItems } from './UserMenuItems'; type OrganizationModel = components['schemas']['OrganizationModel']; diff --git a/webapp/src/component/security/UserMenu/UserUnverifiedEmailMenu.tsx b/webapp/src/component/security/UserMenu/UserUnverifiedEmailMenu.tsx index 5d02a83ac0..3a3973d8fc 100644 --- a/webapp/src/component/security/UserMenu/UserUnverifiedEmailMenu.tsx +++ b/webapp/src/component/security/UserMenu/UserUnverifiedEmailMenu.tsx @@ -1,13 +1,13 @@ import React, { useState } from 'react'; import { IconButton, MenuItem, Popover, styled } from '@mui/material'; -import { UserMenuItems } from 'tg.hooks/useUserMenuItems'; import { UserAvatar } from 'tg.component/common/avatar/UserAvatar'; import { ThemeItem } from './ThemeItem'; import { LanguageItem } from './LanguageItem'; import { T } from '@tolgee/react'; import { useGlobalActions } from 'tg.globalContext/GlobalContext'; +import { UserMenuItems } from './UserMenuItems'; const StyledIconButton = styled(IconButton)` width: 40px; diff --git a/webapp/src/plugin/EePluginType.ts b/webapp/src/plugin/EePluginType.ts index 98ed57a5e5..d400841587 100644 --- a/webapp/src/plugin/EePluginType.ts +++ b/webapp/src/plugin/EePluginType.ts @@ -8,7 +8,8 @@ import { components } from 'tg.service/apiSchema.generated'; import { BatchOperationAdder } from 'tg.views/projects/translations/BatchOperations/operations'; import { addPanel } from 'tg.views/projects/translations/ToolsPanel/panelsList'; import { DeveloperViewItemsAdder } from 'tg.views/projects/developer/developerViewItems'; -import { UserMenuItemsAdder } from 'tg.hooks/useUserMenuItems'; +import { UserMenuItemsAdder } from 'tg.component/security/UserMenu/UserMenuItems'; +import { ProjectMenuItemsAdder } from 'tg.views/projects/projectMenu/ProjectMenu'; export interface EePluginType { ee?: { @@ -40,6 +41,7 @@ export interface EePluginType { translationPanelAdder: ReturnType; useAddDeveloperViewItems: () => DeveloperViewItemsAdder; useAddUserMenuItems: () => UserMenuItemsAdder; + useAddProjectMenuItems: () => ProjectMenuItemsAdder; }; } diff --git a/webapp/src/views/projects/projectMenu/ProjectMenu.tsx b/webapp/src/views/projects/projectMenu/ProjectMenu.tsx index b983d3b761..507d63e475 100644 --- a/webapp/src/views/projects/projectMenu/ProjectMenu.tsx +++ b/webapp/src/views/projects/projectMenu/ProjectMenu.tsx @@ -1,6 +1,5 @@ import { useTranslate } from '@tolgee/react'; import { - ClipboardCheck, Code02, FileDownload03, Globe01, @@ -11,171 +10,179 @@ import { UploadCloud02, User01, } from '@untitled-ui/icons-react'; -import { LINKS, PARAMS } from 'tg.constants/links'; +import { Link, LINKS, PARAMS } from 'tg.constants/links'; import { useConfig } from 'tg.globalContext/helpers'; import { SideMenu } from './SideMenu'; -import { SideMenuItem } from './SideMenuItem'; +import { SideMenuItem, SideMenuItemQuickStart } from './SideMenuItem'; import { SideLogo } from './SideLogo'; import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; import { useGlobalContext } from 'tg.globalContext/GlobalContext'; import { Integration } from 'tg.component/CustomIcons'; +import { FC } from 'react'; +import { createAdder } from 'tg.fixtures/pluginAdder'; +import { getEe } from '../../../plugin/getEe'; export const ProjectMenu = ({ id }) => { const { satisfiesPermission } = useProjectPermissions(); const config = useConfig(); - - const canViewKeys = satisfiesPermission('keys.view'); - const canViewTranslations = satisfiesPermission('translations.view'); - const canEditProject = satisfiesPermission('project.edit'); - const canEditLanguages = satisfiesPermission('languages.edit'); - const canViewUsers = - config.authentication && satisfiesPermission('members.view'); - const canImport = canViewKeys && satisfiesPermission('translations.edit'); - const canIntegrate = canViewKeys; const canPublishCd = satisfiesPermission('content-delivery.publish'); - const canManageWebhooks = satisfiesPermission('webhooks.manage'); - const canViewDeveloper = canPublishCd || canManageWebhooks; - const canViewTasks = satisfiesPermission('tasks.view'); const { t } = useTranslate(); const topBarHeight = useGlobalContext((c) => c.layout.topBarHeight); + const baseItems = [ + { + id: 'projects', + condition: () => true, + link: LINKS.PROJECTS, + icon: HomeLine, + text: t('project_menu_projects'), + dataCy: 'project-menu-item-projects', + }, + { + id: 'dashboard', + condition: () => true, + link: LINKS.PROJECT_DASHBOARD, + icon: LayoutAlt04, + text: t('project_menu_dashboard', 'Project Dashboard'), + dataCy: 'project-menu-item-dashboard', + }, + { + id: 'translations', + condition: ({ satisfiesPermission }) => satisfiesPermission('keys.view'), + link: LINKS.PROJECT_TRANSLATIONS, + icon: Translate01, + text: t('project_menu_translations'), + dataCy: 'project-menu-item-translations', + matchAsPrefix: true, + quickStart: { itemKey: 'menu_translations' }, + }, + { + id: 'languages', + condition: ({ satisfiesPermission }) => + satisfiesPermission('languages.edit'), + link: LINKS.PROJECT_LANGUAGES, + icon: Globe01, + text: t('project_menu_languages'), + dataCy: 'project-menu-item-languages', + matchAsPrefix: true, + quickStart: { itemKey: 'menu_languages' }, + }, + { + id: 'members', + condition: ({ satisfiesPermission }) => + config.authentication && satisfiesPermission('members.view'), + link: LINKS.PROJECT_PERMISSIONS, + icon: User01, + text: t('project_menu_members'), + dataCy: 'project-menu-item-members', + quickStart: { itemKey: 'menu_members' }, + }, + { + id: 'import', + condition: ({ satisfiesPermission }) => + satisfiesPermission('translations.edit') && + satisfiesPermission('keys.view'), + link: LINKS.PROJECT_IMPORT, + icon: UploadCloud02, + text: t('project_menu_import'), + dataCy: 'project-menu-item-import', + quickStart: { itemKey: 'menu_import' }, + }, + { + id: 'export', + condition: ({ satisfiesPermission }) => + satisfiesPermission('translations.view'), + link: LINKS.PROJECT_EXPORT, + icon: FileDownload03, + text: t('project_menu_export'), + dataCy: 'project-menu-item-export', + quickStart: { itemKey: 'menu_export' }, + }, + { + id: 'developer', + condition: ({ satisfiesPermission }) => + satisfiesPermission('content-delivery.publish') || + satisfiesPermission('webhooks.manage'), + link: canPublishCd + ? LINKS.PROJECT_CONTENT_STORAGE + : LINKS.PROJECT_WEBHOOKS, + icon: Code02, + text: t('project_menu_developer'), + dataCy: 'project-menu-item-developer', + quickStart: { itemKey: 'menu_developer' }, + matchAsPrefix: LINKS.PROJECT_DEVELOPER.build({ [PARAMS.PROJECT_ID]: id }), + }, + { + id: 'integrate', + condition: ({ satisfiesPermission }) => satisfiesPermission('keys.view'), + link: LINKS.PROJECT_INTEGRATE, + icon: Integration, + text: t('project_menu_integrate'), + dataCy: 'project-menu-item-integrate', + quickStart: { itemKey: 'menu_integrate' }, + }, + { + id: 'settings', + condition: ({ satisfiesPermission }) => + satisfiesPermission('project.edit'), + link: LINKS.PROJECT_EDIT, + icon: Settings01, + text: t('project_menu_project_settings'), + dataCy: 'project-menu-item-settings', + matchAsPrefix: true, + quickStart: { itemKey: 'menu_settings' }, + }, + ] satisfies ProjectMenuItem[]; + + const { useAddProjectMenuItems } = getEe(); + + const addEeItems = useAddProjectMenuItems(); + + const items = addEeItems(baseItems); + return ( ); }; + +export type ProjectMenuItem = { + id: string; + condition: (props: ConditionProps) => boolean; + link: Link; + icon: FC; + text: string; + dataCy: string; + matchAsPrefix?: boolean | string; + quickStart?: SideMenuItemQuickStart; +}; + +type ConditionProps = { + config: ReturnType; + satisfiesPermission: ReturnType< + typeof useProjectPermissions + >['satisfiesPermission']; +}; + +export const addProjectMenuItems = createAdder({ + referencingProperty: 'id', +}); + +export type ProjectMenuItemsAdder = ReturnType; diff --git a/webapp/src/views/projects/projectMenu/SideMenuItem.tsx b/webapp/src/views/projects/projectMenu/SideMenuItem.tsx index 0904fbef6b..750a4134b6 100644 --- a/webapp/src/views/projects/projectMenu/SideMenuItem.tsx +++ b/webapp/src/views/projects/projectMenu/SideMenuItem.tsx @@ -40,21 +40,21 @@ type Props = { linkTo?: string; icon: React.ReactElement; text: string; - selected?: boolean; matchAsPrefix?: boolean | string; hidden?: boolean; 'data-cy': string; - quickStart?: Omit< - React.ComponentProps, - 'children' - >; + quickStart?: SideMenuItemQuickStart; }; +export type SideMenuItemQuickStart = Omit< + React.ComponentProps, + 'children' +>; + export function SideMenuItem({ linkTo, icon, text, - selected, matchAsPrefix, hidden, quickStart, @@ -62,15 +62,13 @@ export function SideMenuItem({ }: Props) { const match = useLocation(); - const isSelected = selected - ? true - : matchAsPrefix + const isSelected = matchAsPrefix ? match.pathname.startsWith( typeof matchAsPrefix === 'string' ? matchAsPrefix : String(linkTo) ) : match.pathname === linkTo; - const matchesExactly = match.pathname === linkTo || selected; + const matchesExactly = match.pathname === linkTo; function wrapWithQuickStart(children: React.ReactNode) { if (quickStart) {