Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| effac1a391 |
@@ -24,8 +24,8 @@ import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/con
|
||||
import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
|
||||
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
|
||||
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
|
||||
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
|
||||
@@ -46,7 +46,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PageLayoutType } from '~/generated-metadata/graphql';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
@@ -100,7 +100,9 @@ export const PageLayoutTabList = ({
|
||||
const { t } = useLingui();
|
||||
const { isCustom } = useIsCurrentObjectCustom();
|
||||
|
||||
const shouldTranslateTabTitles = !isCustom;
|
||||
const isRecordPageLayout = pageLayoutType === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const shouldTranslateTabTitles = isRecordPageLayout && !isCustom;
|
||||
|
||||
const tabsWithIcons: SingleTabProps[] = tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { msg, type MacroMessageDescriptor } from '@lingui/core/macro';
|
||||
|
||||
// Translation map for standard page layout widget titles.
|
||||
// When the backend hasn't translated the widget title (e.g. when using
|
||||
// frontend fallback layouts), this map is used to translate known
|
||||
// standard widget titles at the rendering point.
|
||||
// Custom/user-created widget titles that are not in this map will be
|
||||
// displayed as-is.
|
||||
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
|
||||
export const STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS: Record<
|
||||
string,
|
||||
MacroMessageDescriptor
|
||||
> = {
|
||||
Fields: msg`Fields`,
|
||||
Timeline: msg`Timeline`,
|
||||
Tasks: msg`Tasks`,
|
||||
Notes: msg`Notes`,
|
||||
Files: msg`Files`,
|
||||
Emails: msg`Emails`,
|
||||
Calendar: msg`Calendar`,
|
||||
Note: msg`Note`,
|
||||
Task: msg`Task`,
|
||||
Flow: msg`Flow`,
|
||||
};
|
||||
+18
-1
@@ -1,7 +1,9 @@
|
||||
import { STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutWidgetTitleTranslations';
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
|
||||
import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
@@ -26,6 +28,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
|
||||
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
@@ -47,8 +50,10 @@ type WidgetRendererProps = {
|
||||
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { handleEditWidget } = useEditPageLayoutWidget();
|
||||
const { isCustom } = useIsCurrentObjectCustom();
|
||||
|
||||
const isPageLayoutInEditMode = useAtomComponentStateValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
@@ -89,6 +94,9 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
const isDeletingWidgetEnabled =
|
||||
currentPageLayout.type !== PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isRecordPageLayout =
|
||||
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
// TODO: when we have more widgets without headers, we should use a more generic approach to hide the header
|
||||
// each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never')
|
||||
const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT;
|
||||
@@ -132,6 +140,15 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
|
||||
const actions = useWidgetActions({ widget });
|
||||
|
||||
const shouldTranslateWidgetTitles = isRecordPageLayout && !isCustom;
|
||||
|
||||
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
|
||||
const translatedWidgetTitle =
|
||||
shouldTranslateWidgetTitles &&
|
||||
STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS[widget.title]
|
||||
? t(STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS[widget.title])
|
||||
: widget.title;
|
||||
|
||||
return (
|
||||
<WidgetComponentInstanceContext.Provider value={{ instanceId: widget.id }}>
|
||||
<WidgetCard
|
||||
@@ -157,7 +174,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
isResizing={isResizing}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
|
||||
title={widget.title}
|
||||
title={translatedWidgetTitle}
|
||||
onRemove={handleRemove}
|
||||
actions={actions}
|
||||
forbiddenDisplay={
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { PageLayoutTabController } from 'src/engine/metadata-modules/page-layout
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutTabResolver } from 'src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
|
||||
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -31,6 +32,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
|
||||
FlatPageLayoutWidgetModule,
|
||||
ApplicationModule,
|
||||
DashboardSyncModule,
|
||||
PageLayoutModule,
|
||||
],
|
||||
controllers: [PageLayoutTabController],
|
||||
providers: [
|
||||
|
||||
+9
@@ -29,6 +29,7 @@ import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layou
|
||||
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 { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
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';
|
||||
|
||||
@@ -40,6 +41,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
|
||||
export class PageLayoutTabResolver {
|
||||
constructor(
|
||||
private readonly pageLayoutTabService: PageLayoutTabService,
|
||||
private readonly pageLayoutService: PageLayoutService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@@ -47,12 +49,19 @@ export class PageLayoutTabResolver {
|
||||
async title(
|
||||
@Parent() tab: PageLayoutTabDTO,
|
||||
@Context() context: I18nContext,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<string> {
|
||||
const i18n = this.i18nService.getI18nInstance(context.req.locale);
|
||||
|
||||
const pageLayout = await this.pageLayoutService.findByIdOrThrow({
|
||||
id: tab.pageLayoutId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return resolvePageLayoutTabTitle({
|
||||
title: tab.title,
|
||||
applicationId: tab.applicationId,
|
||||
pageLayoutType: pageLayout.type,
|
||||
i18nInstance: i18n,
|
||||
});
|
||||
}
|
||||
|
||||
+38
@@ -2,6 +2,7 @@ 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 { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
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');
|
||||
@@ -27,6 +28,7 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
@@ -42,6 +44,7 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'My Custom Tab',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
@@ -57,6 +60,7 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: '',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
@@ -84,6 +88,7 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: source,
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
@@ -100,6 +105,39 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: customAppId,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Home');
|
||||
});
|
||||
|
||||
it('should not translate title when pageLayoutType is DASHBOARD', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Accueil');
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.DASHBOARD,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Home');
|
||||
});
|
||||
|
||||
it('should not translate title when pageLayoutType is RECORD_INDEX', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Accueil');
|
||||
|
||||
const result = resolvePageLayoutTabTitle({
|
||||
title: 'Home',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_INDEX,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
|
||||
+7
@@ -1,17 +1,24 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
export const resolvePageLayoutTabTitle = ({
|
||||
title,
|
||||
applicationId,
|
||||
pageLayoutType,
|
||||
i18nInstance,
|
||||
}: {
|
||||
title: string;
|
||||
applicationId: string;
|
||||
pageLayoutType: PageLayoutType;
|
||||
i18nInstance: I18n;
|
||||
}): string => {
|
||||
if (pageLayoutType !== PageLayoutType.RECORD_PAGE) {
|
||||
return title;
|
||||
}
|
||||
|
||||
if (applicationId !== TWENTY_STANDARD_APPLICATION.universalIdentifier) {
|
||||
return title;
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
// This file exists solely for Lingui string extraction.
|
||||
// The strings defined here correspond to standard page layout widget titles
|
||||
// so they appear in the .po catalogs and can be translated at resolve time
|
||||
// via generateMessageId hash lookups.
|
||||
export const getStandardPageLayoutWidgetTitles = () => [
|
||||
t`Fields`,
|
||||
t`Timeline`,
|
||||
t`Tasks`,
|
||||
t`Notes`,
|
||||
t`Files`,
|
||||
t`Emails`,
|
||||
t`Calendar`,
|
||||
t`Note`,
|
||||
t`Task`,
|
||||
t`Flow`,
|
||||
];
|
||||
+5
-2
@@ -3,8 +3,8 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import {
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
PageLayoutWidgetConditionalDisplay,
|
||||
PageLayoutWidgetPosition,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
@@ -35,6 +35,9 @@ export class PageLayoutWidgetDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
applicationId: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
pageLayoutTabId: string;
|
||||
|
||||
|
||||
+4
@@ -6,10 +6,12 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatPageLayoutWidgetModule } from 'src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module';
|
||||
import { PageLayoutTabModule } from 'src/engine/metadata-modules/page-layout-tab/page-layout-tab.module';
|
||||
import { PageLayoutWidgetController } from 'src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
import { PageLayoutWidgetResolver } from 'src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
|
||||
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -29,6 +31,8 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
|
||||
FlatPageLayoutWidgetModule,
|
||||
ApplicationModule,
|
||||
DashboardSyncModule,
|
||||
PageLayoutModule,
|
||||
PageLayoutTabModule,
|
||||
],
|
||||
controllers: [PageLayoutWidgetController],
|
||||
providers: [
|
||||
|
||||
+43
-2
@@ -4,22 +4,34 @@ import {
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Parent, Query, ResolveField } 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';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
|
||||
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
|
||||
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
|
||||
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
|
||||
import { resolvePageLayoutWidgetTitle } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-page-layout-widget-title.util';
|
||||
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
|
||||
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';
|
||||
|
||||
@@ -31,8 +43,37 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
|
||||
export class PageLayoutWidgetResolver {
|
||||
constructor(
|
||||
private readonly pageLayoutWidgetService: PageLayoutWidgetService,
|
||||
private readonly pageLayoutTabService: PageLayoutTabService,
|
||||
private readonly pageLayoutService: PageLayoutService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@ResolveField(() => String)
|
||||
async title(
|
||||
@Parent() widget: PageLayoutWidgetDTO,
|
||||
@Context() context: I18nContext,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<string> {
|
||||
const i18n = this.i18nService.getI18nInstance(context.req.locale);
|
||||
|
||||
const tab = await this.pageLayoutTabService.findByIdOrThrow({
|
||||
id: widget.pageLayoutTabId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const pageLayout = await this.pageLayoutService.findByIdOrThrow({
|
||||
id: tab.pageLayoutId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return resolvePageLayoutWidgetTitle({
|
||||
title: widget.title,
|
||||
applicationId: widget.applicationId,
|
||||
pageLayoutType: pageLayout.type,
|
||||
i18nInstance: i18n,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [PageLayoutWidgetDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async getPageLayoutWidgets(
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { resolvePageLayoutWidgetTitle } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-page-layout-widget-title.util';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
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('resolvePageLayoutWidgetTitle', () => {
|
||||
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('Champs');
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: 'Fields',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).toHaveBeenCalledWith('Fields');
|
||||
expect(mockI18n._).toHaveBeenCalledWith('abc123');
|
||||
expect(result).toBe('Champs');
|
||||
});
|
||||
|
||||
it('should return original title when catalog returns the hash (no translation found)', () => {
|
||||
mockGenerateMessageId.mockReturnValue('xyz789');
|
||||
mockI18n._.mockReturnValue('xyz789');
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: 'My Custom Widget',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).toHaveBeenCalledWith('My Custom Widget');
|
||||
expect(mockI18n._).toHaveBeenCalledWith('xyz789');
|
||||
expect(result).toBe('My Custom Widget');
|
||||
});
|
||||
|
||||
it('should return original title for empty string', () => {
|
||||
mockGenerateMessageId.mockReturnValue('empty-hash');
|
||||
mockI18n._.mockReturnValue('empty-hash');
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: '',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should translate standard widget titles', () => {
|
||||
const standardWidgets = [
|
||||
{ source: 'Fields', translated: 'Champs' },
|
||||
{ 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: 'Task', translated: 'Tâche' },
|
||||
{ source: 'Flow', translated: 'Flux' },
|
||||
];
|
||||
|
||||
standardWidgets.forEach(({ source, translated }) => {
|
||||
jest.clearAllMocks();
|
||||
mockGenerateMessageId.mockReturnValue(`hash-${source}`);
|
||||
mockI18n._.mockReturnValue(translated);
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: source,
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(result).toBe(translated);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not translate title when applicationId is not from standard app', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Champs');
|
||||
|
||||
const customAppId = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: 'Fields',
|
||||
applicationId: customAppId,
|
||||
pageLayoutType: PageLayoutType.RECORD_PAGE,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Fields');
|
||||
});
|
||||
|
||||
it('should not translate title when pageLayoutType is DASHBOARD', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Champs');
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: 'Fields',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.DASHBOARD,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Fields');
|
||||
});
|
||||
|
||||
it('should not translate title when pageLayoutType is RECORD_INDEX', () => {
|
||||
mockGenerateMessageId.mockReturnValue('abc123');
|
||||
mockI18n._.mockReturnValue('Champs');
|
||||
|
||||
const result = resolvePageLayoutWidgetTitle({
|
||||
title: 'Fields',
|
||||
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
pageLayoutType: PageLayoutType.RECORD_INDEX,
|
||||
i18nInstance: mockI18n,
|
||||
});
|
||||
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
expect(result).toBe('Fields');
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
export const resolvePageLayoutWidgetTitle = ({
|
||||
title,
|
||||
applicationId,
|
||||
pageLayoutType,
|
||||
i18nInstance,
|
||||
}: {
|
||||
title: string;
|
||||
applicationId: string;
|
||||
pageLayoutType: PageLayoutType;
|
||||
i18nInstance: I18n;
|
||||
}): string => {
|
||||
if (pageLayoutType !== PageLayoutType.RECORD_PAGE) {
|
||||
return title;
|
||||
}
|
||||
|
||||
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