Translate page layout tab title (#17975)
> [!NOTE] > The improvement will only fully work once all tabs get translated. ## When using the front-end mocks https://github.com/user-attachments/assets/cf7dc0fb-9438-4e18-841e-47558ff71474 ## When loading page layout information from database https://github.com/user-attachments/assets/d2c9d98b-97e2-4629-aa6c-53f3f3713733 ## When editing dashboard https://github.com/user-attachments/assets/c6ae3a7a-f05f-48ea-8b33-8f689e3f71f7 Closes https://github.com/twentyhq/twenty/issues/17950#issuecomment-3902782952
This commit is contained in:
committed by
GitHub
parent
e3fcff00b0
commit
963f2de864
File diff suppressed because one or more lines are too long
+1
@@ -5,6 +5,7 @@ export const PAGE_LAYOUT_TAB_FRAGMENT = gql`
|
||||
${PAGE_LAYOUT_WIDGET_FRAGMENT}
|
||||
fragment PageLayoutTabFragment on PageLayoutTab {
|
||||
id
|
||||
applicationId
|
||||
title
|
||||
icon
|
||||
position
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type OnDragUpdateResponder,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IconPlus, useIcons } from 'twenty-ui/display';
|
||||
@@ -32,6 +33,8 @@ import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/Pag
|
||||
import { PageLayoutTabListReorderableOverflowDropdown } from '@/page-layout/components/PageLayoutTabListReorderableOverflowDropdown';
|
||||
import { PageLayoutTabListStaticOverflowDropdown } from '@/page-layout/components/PageLayoutTabListStaticOverflowDropdown';
|
||||
import { PageLayoutTabListVisibleTabs } from '@/page-layout/components/PageLayoutTabListVisibleTabs';
|
||||
import { STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutTabTitleTranslations';
|
||||
import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { pageLayoutTabListCurrentDragDroppableIdComponentState } from '@/page-layout/states/pageLayoutTabListCurrentDragDroppableIdComponentState';
|
||||
@@ -94,10 +97,19 @@ export const PageLayoutTabList = ({
|
||||
pageLayoutType,
|
||||
}: PageLayoutTabListProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { t } = useLingui();
|
||||
const { isCustom } = useIsCurrentObjectCustom();
|
||||
|
||||
const shouldTranslateTabTitles = !isCustom;
|
||||
|
||||
const tabsWithIcons: SingleTabProps[] = tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
|
||||
title:
|
||||
shouldTranslateTabTitles &&
|
||||
STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS[tab.title]
|
||||
? t(STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS[tab.title])
|
||||
: tab.title,
|
||||
Icon: tab.icon ? getIcon(tab.icon) : undefined,
|
||||
}));
|
||||
|
||||
|
||||
+4
@@ -22,6 +22,7 @@ const StyledContainer = styled.div`
|
||||
const createInitialTabs = (): PageLayoutTab[] => [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'overview',
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
@@ -34,6 +35,7 @@ const createInitialTabs = (): PageLayoutTab[] => [
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'revenue',
|
||||
title: 'Revenue',
|
||||
position: 1,
|
||||
@@ -45,6 +47,7 @@ const createInitialTabs = (): PageLayoutTab[] => [
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'forecasts',
|
||||
title: 'Forecasts',
|
||||
position: 2,
|
||||
@@ -73,6 +76,7 @@ const PageLayoutTabListPlayground = ({
|
||||
...prev,
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: `new-tab-${nextIndex}`,
|
||||
title: `New Tab ${nextIndex}`,
|
||||
position: nextIndex,
|
||||
|
||||
+7
@@ -19,6 +19,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -56,6 +57,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -93,6 +95,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-tasks',
|
||||
title: 'Tasks',
|
||||
icon: 'IconCheckbox',
|
||||
@@ -130,6 +133,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-notes',
|
||||
title: 'Notes',
|
||||
icon: 'IconNotes',
|
||||
@@ -167,6 +171,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
@@ -204,6 +209,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-emails',
|
||||
title: 'Emails',
|
||||
icon: 'IconMail',
|
||||
@@ -241,6 +247,7 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'company-tab-calendar',
|
||||
title: 'Calendar',
|
||||
icon: 'IconCalendarEvent',
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'note-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -88,6 +89,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'note-tab-note',
|
||||
title: 'Note',
|
||||
icon: 'IconNotes',
|
||||
@@ -132,6 +134,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'note-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -169,6 +172,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'note-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
|
||||
+7
@@ -19,6 +19,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -56,6 +57,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -93,6 +95,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-tasks',
|
||||
title: 'Tasks',
|
||||
icon: 'IconCheckbox',
|
||||
@@ -130,6 +133,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-notes',
|
||||
title: 'Notes',
|
||||
icon: 'IconNotes',
|
||||
@@ -167,6 +171,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
@@ -204,6 +209,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-emails',
|
||||
title: 'Emails',
|
||||
icon: 'IconMail',
|
||||
@@ -241,6 +247,7 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'opportunity-tab-calendar',
|
||||
title: 'Calendar',
|
||||
icon: 'IconCalendarEvent',
|
||||
|
||||
+7
@@ -18,6 +18,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -55,6 +56,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -92,6 +94,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-tasks',
|
||||
title: 'Tasks',
|
||||
icon: 'IconCheckbox',
|
||||
@@ -129,6 +132,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-notes',
|
||||
title: 'Notes',
|
||||
icon: 'IconNotes',
|
||||
@@ -166,6 +170,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
@@ -203,6 +208,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-emails',
|
||||
title: 'Emails',
|
||||
icon: 'IconMail',
|
||||
@@ -240,6 +246,7 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'person-tab-calendar',
|
||||
title: 'Calendar',
|
||||
icon: 'IconCalendarEvent',
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'default-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -55,6 +56,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'default-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -92,6 +94,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'default-tab-tasks',
|
||||
title: 'Tasks',
|
||||
icon: 'IconCheckbox',
|
||||
@@ -129,6 +132,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'default-tab-notes',
|
||||
title: 'Notes',
|
||||
icon: 'IconNotes',
|
||||
@@ -166,6 +170,7 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'default-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'task-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -85,6 +86,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'task-tab-note',
|
||||
title: 'Note',
|
||||
icon: 'IconNotes',
|
||||
@@ -129,6 +131,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'task-tab-timeline',
|
||||
title: 'Timeline',
|
||||
icon: 'IconTimelineEvent',
|
||||
@@ -166,6 +169,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'task-tab-files',
|
||||
title: 'Files',
|
||||
icon: 'IconPaperclip',
|
||||
|
||||
@@ -23,6 +23,7 @@ export const DEFAULT_WORKFLOW_PAGE_LAYOUT: PageLayout = {
|
||||
// Flow tab (position 100)
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'workflow-tab-flow',
|
||||
title: 'Flow',
|
||||
position: 100,
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
|
||||
// Fields tab (position 100)
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'workflow-run-tab-fields',
|
||||
title: 'Home',
|
||||
position: 100,
|
||||
@@ -62,6 +63,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
|
||||
// Flow tab (position 200)
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'workflow-run-tab-flow',
|
||||
title: 'Flow',
|
||||
position: 200,
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
|
||||
// Fields tab (position 100)
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'workflow-version-tab-fields',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
@@ -62,6 +63,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
|
||||
// Flow tab (position 200)
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'workflow-version-tab-flow',
|
||||
title: 'Flow',
|
||||
icon: 'IconSettings',
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { msg, type MacroMessageDescriptor } from '@lingui/core/macro';
|
||||
|
||||
// Translation map for standard page layout tab titles.
|
||||
// When the backend hasn't translated the tab title (e.g. when using
|
||||
// frontend fallback layouts), this map is used to translate known
|
||||
// standard tab titles at the rendering point.
|
||||
// Custom/user-created tab titles that are not in this map will be
|
||||
// displayed as-is.
|
||||
export const STANDARD_PAGE_LAYOUT_TAB_TITLE_TRANSLATIONS: Record<
|
||||
string,
|
||||
MacroMessageDescriptor
|
||||
> = {
|
||||
Home: msg`Home`,
|
||||
Timeline: msg`Timeline`,
|
||||
Tasks: msg`Tasks`,
|
||||
Notes: msg`Notes`,
|
||||
Files: msg`Files`,
|
||||
Emails: msg`Emails`,
|
||||
Calendar: msg`Calendar`,
|
||||
Note: msg`Note`,
|
||||
Flow: msg`Flow`,
|
||||
};
|
||||
+1
@@ -18,6 +18,7 @@ export const UPDATE_PAGE_LAYOUT_WITH_TABS_AND_WIDGETS = gql`
|
||||
deletedAt
|
||||
tabs {
|
||||
id
|
||||
applicationId
|
||||
title
|
||||
position
|
||||
pageLayoutId
|
||||
|
||||
+3
-1
@@ -9,9 +9,9 @@ import { act, renderHook } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
WidgetType,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
@@ -73,6 +73,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
@@ -152,6 +153,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
@@ -6,7 +7,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
PageLayoutTestWrapper,
|
||||
@@ -228,6 +228,7 @@ describe('useCreatePageLayoutTab', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'existing-tab',
|
||||
applicationId: '',
|
||||
title: 'Existing Tab',
|
||||
position: 0,
|
||||
pageLayoutId: 'test-layout',
|
||||
|
||||
+3
-2
@@ -1,12 +1,12 @@
|
||||
import { usePageLayoutDraftState } from '@/page-layout/hooks/usePageLayoutDraftState';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
GraphOrderBy,
|
||||
WidgetType,
|
||||
AggregateOperations,
|
||||
BarChartLayout,
|
||||
GraphOrderBy,
|
||||
PageLayoutType,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
@@ -88,6 +88,7 @@ describe('usePageLayoutDraftState', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: '',
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ describe('usePageLayoutWithRelationWidgets', () => {
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'tab-1',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { getEmptyTabLayout } from '@/page-layout/utils/getEmptyTabLayout';
|
||||
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@@ -6,10 +10,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { getEmptyTabLayout } from '@/page-layout/utils/getEmptyTabLayout';
|
||||
|
||||
export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -48,6 +48,7 @@ export const useCreatePageLayoutTab = (pageLayoutIdFromProps?: string) => {
|
||||
: -1;
|
||||
const newTab: PageLayoutTab = {
|
||||
id: newTabId,
|
||||
applicationId: '',
|
||||
title: title || `Tab ${tabsLength + 1}`,
|
||||
position: maxPosition + 1,
|
||||
pageLayoutId: pageLayoutId,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useIsCurrentObjectCustom = () => {
|
||||
const { targetRecordIdentifier } = useLayoutRenderingContext();
|
||||
|
||||
const objectMetadataItem = useRecoilValue(
|
||||
objectMetadataItemFamilySelector({
|
||||
objectName: targetRecordIdentifier?.targetObjectNameSingular ?? '',
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(targetRecordIdentifier)) {
|
||||
return { isCustom: true };
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return { isCustom: true };
|
||||
}
|
||||
|
||||
return { isCustom: objectMetadataItem.isCustom };
|
||||
};
|
||||
@@ -30,6 +30,7 @@ describe('addWidgetToTab', () => {
|
||||
const mockTabs: PageLayoutTab[] = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
@@ -40,6 +41,7 @@ describe('addWidgetToTab', () => {
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
applicationId: '',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
pageLayoutId: 'layout-1',
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ describe('convertPageLayoutToTabLayouts', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'page-layout-1',
|
||||
@@ -97,6 +98,7 @@ describe('convertPageLayoutToTabLayouts', () => {
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'page-layout-1',
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ import { getPageLayoutTabListInitialActiveTabId } from '@/page-layout/utils/getP
|
||||
describe('getPageLayoutTabListInitialActiveTabId', () => {
|
||||
const createMockTab = (id: string): PageLayoutTab => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
pageLayoutId: 'page-layout-1',
|
||||
title: `Tab ${id}`,
|
||||
position: 0,
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ import {
|
||||
describe('getTabsByDisplayMode', () => {
|
||||
const createMockTab = (id: string): PageLayoutTab => ({
|
||||
id,
|
||||
applicationId: '',
|
||||
pageLayoutId: 'page-layout-1',
|
||||
title: `Tab ${id}`,
|
||||
position: 0,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ describe('getTabsWithVisibleWidgets', () => {
|
||||
widgets: PageLayoutTab['widgets'],
|
||||
): PageLayoutTab => ({
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id,
|
||||
pageLayoutId: 'page-layout-1',
|
||||
title: `Tab ${id}`,
|
||||
|
||||
+4
-2
@@ -1,16 +1,17 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import {
|
||||
AggregateOperations,
|
||||
GraphOrderBy,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
|
||||
describe('removeWidgetFromTab', () => {
|
||||
const mockTabs: PageLayoutTab[] = [
|
||||
{
|
||||
id: 'tab-1',
|
||||
applicationId: '',
|
||||
title: 'Tab 1',
|
||||
position: 0,
|
||||
pageLayoutId: 'layout-1',
|
||||
@@ -60,6 +61,7 @@ describe('removeWidgetFromTab', () => {
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
applicationId: '',
|
||||
title: 'Tab 2',
|
||||
position: 1,
|
||||
pageLayoutId: 'layout-1',
|
||||
|
||||
+4
@@ -145,6 +145,7 @@ const createPageLayoutWithWidget = (
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: TAB_ID_OVERVIEW,
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
@@ -1699,6 +1700,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
|
||||
? [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'pinned-tab',
|
||||
title: 'Pinned Tab',
|
||||
position: 0,
|
||||
@@ -1710,6 +1712,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: 'other-tab',
|
||||
title: 'Other Tab',
|
||||
position: 1,
|
||||
@@ -1723,6 +1726,7 @@ export const Catalog: CatalogStory<Story, typeof WidgetRenderer> = {
|
||||
: [
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
applicationId: '',
|
||||
id: TAB_ID_OVERVIEW,
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ import {
|
||||
} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { FieldWidget } from '@/page-layout/widgets/field/components/FieldWidget';
|
||||
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
|
||||
@@ -291,7 +292,7 @@ const TAB_ID_OVERVIEW = 'tab-overview';
|
||||
const createPageLayoutWithWidget = (
|
||||
widget: PageLayoutWidget,
|
||||
objectMetadataId: string,
|
||||
) => ({
|
||||
): PageLayout => ({
|
||||
id: PAGE_LAYOUT_TEST_INSTANCE_ID,
|
||||
name: 'Mock Page Layout',
|
||||
type: PageLayoutType.RECORD_PAGE,
|
||||
@@ -299,6 +300,7 @@ const createPageLayoutWithWidget = (
|
||||
tabs: [
|
||||
{
|
||||
__typename: 'PageLayoutTab' as const,
|
||||
applicationId: '',
|
||||
id: TAB_ID_OVERVIEW,
|
||||
title: 'Overview',
|
||||
position: 0,
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
// This file exists solely for Lingui string extraction.
|
||||
// The strings defined here correspond to standard page layout tab titles
|
||||
// so they appear in the .po catalogs and can be translated at resolve time
|
||||
// via generateMessageId hash lookups.
|
||||
export const getStandardPageLayoutTabTitles = () => [
|
||||
t`Home`,
|
||||
t`Timeline`,
|
||||
t`Tasks`,
|
||||
t`Notes`,
|
||||
t`Files`,
|
||||
t`Emails`,
|
||||
t`Calendar`,
|
||||
t`Note`,
|
||||
t`Flow`,
|
||||
];
|
||||
+3
@@ -15,6 +15,9 @@ export class PageLayoutTabDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
applicationId: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
title: string;
|
||||
|
||||
|
||||
+30
-3
@@ -4,14 +4,23 @@ import {
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@@ -19,6 +28,7 @@ import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layou
|
||||
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab.input';
|
||||
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
|
||||
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
|
||||
@@ -28,7 +38,24 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
export class PageLayoutTabResolver {
|
||||
constructor(private readonly pageLayoutTabService: PageLayoutTabService) {}
|
||||
constructor(
|
||||
private readonly pageLayoutTabService: PageLayoutTabService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@ResolveField(() => String)
|
||||
async title(
|
||||
@Parent() tab: PageLayoutTabDTO,
|
||||
@Context() context: I18nContext,
|
||||
): Promise<string> {
|
||||
const i18n = this.i18nService.getI18nInstance(context.req.locale);
|
||||
|
||||
return resolvePageLayoutTabTitle({
|
||||
title: tab.title,
|
||||
applicationId: tab.applicationId,
|
||||
i18nInstance: i18n,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [PageLayoutTabDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
jest.mock('src/engine/core-modules/i18n/utils/generateMessageId');
|
||||
|
||||
const mockGenerateMessageId = generateMessageId as jest.MockedFunction<
|
||||
typeof generateMessageId
|
||||
>;
|
||||
|
||||
describe('resolvePageLayoutTabTitle', () => {
|
||||
let mockI18n: jest.Mocked<I18n>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockI18n = {
|
||||
_: jest.fn(),
|
||||
} as unknown as jest.Mocked<I18n>;
|
||||
});
|
||||
|
||||
it('should return translated title when catalog has a match', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Accueil');
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).toHaveBeenCalledWith('Home');
|
||||
expect(mockI18n._).toHaveBeenCalledWith('abc123');
|
||||
expect(result).toBe('Accueil');
|
||||
});
|
||||
|
||||
it('should return original title when catalog returns the hash (no translation found)', () => {
|
||||
mockGenerateMessageId.mockReturnValue('xyz789');
|
||||
mockI18n._.mockReturnValue('xyz789');
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'My Custom Tab',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).toHaveBeenCalledWith('My Custom Tab');
|
||||
expect(mockI18n._).toHaveBeenCalledWith('xyz789');
|
||||
expect(result).toBe('My Custom Tab');
|
||||
});
|
||||
|
||||
it('should return original title for empty string', () => {
|
||||
mockGenerateMessageId.mockReturnValue('empty-hash');
|
||||
mockI18n._.mockReturnValue('empty-hash');
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: '',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should translate standard tab titles', () => {
|
||||
const standardTabs = [
|
||||
{ source: 'Home', translated: 'Accueil' },
|
||||
{ source: 'Timeline', translated: 'Chronologie' },
|
||||
{ source: 'Tasks', translated: 'Tâches' },
|
||||
{ source: 'Notes', translated: 'Notes' },
|
||||
{ source: 'Files', translated: 'Fichiers' },
|
||||
{ source: 'Emails', translated: 'E-mails' },
|
||||
{ source: 'Calendar', translated: 'Calendrier' },
|
||||
{ source: 'Note', translated: 'Note' },
|
||||
{ source: 'Flow', translated: 'Flux' },
|
||||
];
|
||||
|
||||
standardTabs.forEach(({ source, translated }) => {
|
||||
jest.clearAllMocks();
|
||||
mockGenerateMessageId.mockReturnValue(`hash-${source}`);
|
||||
mockI18n._.mockReturnValue(translated);
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: source,
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(result).toBe(translated);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not translate title when applicationId is not from standard app', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Accueil');
|
||||
|
||||
const customAppId = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: customAppId,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Home');
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
export const resolvePageLayoutTabTitle = ({
|
||||
title,
|
||||
applicationId,
|
||||
i18nInstance,
|
||||
}: {
|
||||
title: string;
|
||||
applicationId: string;
|
||||
i18nInstance: I18n;
|
||||
}): string => {
|
||||
if (applicationId !== TWENTY_STANDARD_APPLICATION.universalIdentifier) {
|
||||
return title;
|
||||
}
|
||||
|
||||
const messageId = generateMessageId(title);
|
||||
const translatedMessage = i18nInstance._(messageId);
|
||||
|
||||
if (translatedMessage === messageId) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return translatedMessage;
|
||||
};
|
||||
Reference in New Issue
Block a user