From de9af38a6781ea59842e54fe19b7e81d2852c04d Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Fri, 19 Jun 2026 01:37:28 +0200 Subject: [PATCH] Fix view type label casing (#21772) ## Summary Fix the view type label shown in the object Options menu by introducing a shared `ViewType` label map instead of formatting raw enum values at each call site. I chose to fix the root cause instead of only patching the symptom: the menu was displaying the generated enum value `TABLE`, and `capitalize()` only uppercased the first character without lowercasing the rest. The new mapping gives each view type an explicit translated UI label, so the parent Options menu, the Layout submenu, the view picker, and application content rows all use the same casing source. ## Before / After | Before | After | | --- | --- | | The Options menu showed `TABLE` in uppercase. | The Options menu now shows `Table`, and the Layout submenu still shows `Table`, `Calendar`, and `Kanban`. | | ![Before: Options menu displayed TABLE](https://raw.githubusercontent.com/twentyhq/twenty/7b1752e53cfa63868eb8ea46bd74fbadf89f3091/.github/pr-assets/view-type-labels/before.png) | ![After: Options menu displays Table](https://raw.githubusercontent.com/twentyhq/twenty/7b1752e53cfa63868eb8ea46bd74fbadf89f3091/.github/pr-assets/view-type-labels/after.jpg) | ## Tests - `git diff --check` - Browser smoke test on `http://apple.localhost:3001/objects/companies` - default view Options menu still opens - custom view Options menu shows `Layout` contextual text as `Table`, not `TABLE` - Layout submenu still shows `Table`, `Calendar`, and `Kanban` - no browser console errors Not run: package lint/test commands, because this checkout has no `node_modules` installed. --- .../ObjectOptionsDropdownCustomView.tsx | 10 +++++-- .../ObjectOptionsDropdownLayoutContent.tsx | 12 +++++--- ...plicationContentForLayoutAndLogic.test.tsx | 11 +++++++ ...puteApplicationContentForLayoutAndLogic.ts | 6 ++-- .../EndTrialAfterPaymentMethodEffect.tsx | 30 +++++++++---------- .../src/modules/views/types/ViewType.ts | 17 +++++++++++ .../constants/ViewPickerTypeSelectOptions.ts | 13 ++++---- 7 files changed, 70 insertions(+), 29 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx index d32e182f8e..2ecf676cf6 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx @@ -16,13 +16,17 @@ import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSe import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { viewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/viewsFromObjectMetadataItemFamilySelector'; import { ViewKey } from '@/views/types/ViewKey'; -import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; +import { + getViewTypeLabel, + ViewType, + viewTypeIconMapping, +} from '@/views/types/ViewType'; import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { useLingui } from '@lingui/react/macro'; -import { capitalize, isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { IconCalendar, IconCalendarWeek, @@ -143,7 +147,7 @@ export const ObjectOptionsDropdownCustomView = ({ customViewData?.type ?? ViewType.TABLE, )} text={t`Layout`} - contextualText={`${capitalize(customViewData?.type ?? '')}`} + contextualText={t(getViewTypeLabel(customViewData.type))} contextualTextPosition="right" hasSubMenu /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx index fcd177054a..3da7828a2d 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx @@ -17,7 +17,11 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { type GraphQLView } from '@/views/types/GraphQLView'; -import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; +import { + getViewTypeLabel, + ViewType, + viewTypeIconMapping, +} from '@/views/types/ViewType'; import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar'; import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -161,7 +165,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { > { @@ -179,7 +183,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { > { > { objectUniversalIdentifier: personObject.universalIdentifier, icon: 'IconTable', }, + { + universalIdentifier: 'v-2', + name: 'Record table widget', + type: 'TABLE_WIDGET', + objectUniversalIdentifier: personObject.universalIdentifier, + }, ], } as unknown as Manifest; @@ -109,6 +115,11 @@ describe('useComputeApplicationContentForLayoutAndLogic', () => { expect(row.icon).toBe('IconTable'); expect(row.secondary).toContain('Table'); expect(row.secondary).toContain(personObject.labelSingular); + + expect(result.current.viewRows[1].secondary).toContain('Table widget'); + expect(result.current.viewRows[1].secondary).not.toContain( + 'Table_widget', + ); }); }); diff --git a/packages/twenty-front/src/modules/settings/applications/hooks/useComputeApplicationContentForLayoutAndLogic.ts b/packages/twenty-front/src/modules/settings/applications/hooks/useComputeApplicationContentForLayoutAndLogic.ts index 71c993e522..4c9a315461 100644 --- a/packages/twenty-front/src/modules/settings/applications/hooks/useComputeApplicationContentForLayoutAndLogic.ts +++ b/packages/twenty-front/src/modules/settings/applications/hooks/useComputeApplicationContentForLayoutAndLogic.ts @@ -1,9 +1,11 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { getViewTypeLabel } from '@/views/types/ViewType'; +import { i18n } from '@lingui/core'; import { t } from '@lingui/core/macro'; import { type Manifest } from 'twenty-shared/application'; import { SettingsPath } from 'twenty-shared/types'; -import { capitalize, getSettingsPath, isDefined } from 'twenty-shared/utils'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { type Application } from '~/generated-metadata/graphql'; import { type ApplicationContentRow } from '~/pages/settings/applications/components/SettingsApplicationContentSubtable'; @@ -60,7 +62,7 @@ export const useComputeApplicationContentForLayoutAndLogic = ({ const viewRows: ApplicationContentRow[] = (manifestContent?.views ?? []).map( (view) => { const objectLabel = resolveLabel(view.objectUniversalIdentifier); - const formattedType = capitalize((view.type ?? 'TABLE').toLowerCase()); + const formattedType = i18n._(getViewTypeLabel(view.type)); return { key: view.universalIdentifier, diff --git a/packages/twenty-front/src/modules/settings/billing/effect-components/EndTrialAfterPaymentMethodEffect.tsx b/packages/twenty-front/src/modules/settings/billing/effect-components/EndTrialAfterPaymentMethodEffect.tsx index 545fad567e..d0ca001667 100644 --- a/packages/twenty-front/src/modules/settings/billing/effect-components/EndTrialAfterPaymentMethodEffect.tsx +++ b/packages/twenty-front/src/modules/settings/billing/effect-components/EndTrialAfterPaymentMethodEffect.tsx @@ -8,7 +8,7 @@ import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { isNonEmptyString } from '@sniptt/guards'; import { t } from '@lingui/core/macro'; -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; import { SubscriptionStatus } from '~/generated-metadata/graphql'; @@ -24,7 +24,9 @@ export const EndTrialAfterPaymentMethodEffect = () => { const searchParams = new URLSearchParams(location.search); const askAiThreadId = searchParams.get(ASK_AI_THREAD_ID_QUERY_PARAM); - const cleanUpQueryParams = () => { + const cleanUpQueryParams = useCallback(() => { + const searchParams = new URLSearchParams(location.search); + searchParams.delete(START_SUBSCRIPTION_AFTER_PAYMENT_METHOD_QUERY_PARAM); searchParams.delete(ASK_AI_THREAD_ID_QUERY_PARAM); @@ -34,9 +36,9 @@ export const EndTrialAfterPaymentMethodEffect = () => { `${location.pathname}${cleanedSearch.length > 0 ? `?${cleanedSearch}` : ''}${location.hash}`, { replace: true }, ); - }; + }, [location.hash, location.pathname, location.search, navigate]); - const startSubscription = async () => { + const startSubscription = useCallback(async () => { if (subscriptionStatus !== SubscriptionStatus.Trialing) { cleanUpQueryParams(); return; @@ -65,23 +67,21 @@ export const EndTrialAfterPaymentMethodEffect = () => { cleanUpQueryParams(); jotaiStore.set(isEndingSubscriptionTrialPeriodState.atom, false); } - }; + }, [ + askAiThreadId, + cleanUpQueryParams, + endTrialPeriod, + enqueueErrorSnackBar, + openAskAiThread, + subscriptionStatus, + ]); useEffect(() => { if (!isDefined(subscriptionStatus)) { return; } void startSubscription(); - }, [ - location.search, - location.pathname, - location.hash, - navigate, - subscriptionStatus, - endTrialPeriod, - openAskAiThread, - enqueueErrorSnackBar, - ]); + }, [startSubscription, subscriptionStatus]); return null; }; diff --git a/packages/twenty-front/src/modules/views/types/ViewType.ts b/packages/twenty-front/src/modules/views/types/ViewType.ts index 9c8663e1c0..1b094ad79f 100644 --- a/packages/twenty-front/src/modules/views/types/ViewType.ts +++ b/packages/twenty-front/src/modules/views/types/ViewType.ts @@ -1,3 +1,5 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; import { IconCalendar, type IconComponent, @@ -8,6 +10,21 @@ import { export { ViewType } from '~/generated-metadata/graphql'; import { ViewType } from '~/generated-metadata/graphql'; +type ViewTypeLabelKey = `${ViewType}`; + +export const VIEW_TYPE_LABELS = { + [ViewType.TABLE]: msg`Table`, + [ViewType.KANBAN]: msg`Kanban`, + [ViewType.CALENDAR]: msg`Calendar`, + [ViewType.FIELDS_WIDGET]: msg`Fields widget`, + [ViewType.TABLE_WIDGET]: msg`Table widget`, +} satisfies Record; + +export const getViewTypeLabel = ( + viewType: ViewTypeLabelKey = ViewType.TABLE, +): MessageDescriptor => + VIEW_TYPE_LABELS[viewType] ?? VIEW_TYPE_LABELS[ViewType.TABLE]; + const VIEW_TYPE_ICON_MAPPING = [ { icon: IconLayoutKanban, value: ViewType.KANBAN }, { icon: IconTable, value: ViewType.TABLE }, diff --git a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts index 39f86b7534..4c8bbffd15 100644 --- a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts +++ b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts @@ -1,20 +1,23 @@ -import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; -import { msg } from '@lingui/core/macro'; +import { + VIEW_TYPE_LABELS, + ViewType, + viewTypeIconMapping, +} from '@/views/types/ViewType'; export const VIEW_PICKER_TYPE_SELECT_OPTIONS = [ { value: ViewType.TABLE, - label: msg`Table`, + label: VIEW_TYPE_LABELS[ViewType.TABLE], Icon: viewTypeIconMapping(ViewType.TABLE), }, { value: ViewType.KANBAN, - label: msg`Kanban`, + label: VIEW_TYPE_LABELS[ViewType.KANBAN], Icon: viewTypeIconMapping(ViewType.KANBAN), }, { value: ViewType.CALENDAR, - label: msg`Calendar`, + label: VIEW_TYPE_LABELS[ViewType.CALENDAR], Icon: viewTypeIconMapping(ViewType.CALENDAR), }, ];