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.
This commit is contained in:
Thomas des Francs
2026-06-19 01:37:28 +02:00
committed by GitHub
parent 40e386bfb4
commit de9af38a67
7 changed files with 70 additions and 29 deletions
@@ -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
/>
@@ -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 = () => {
>
<MenuItemSelect
LeftIcon={IconTable}
text={t`Table`}
text={t(getViewTypeLabel(ViewType.TABLE))}
selected={currentView?.type === ViewType.TABLE}
focused={selectedItemId === ViewType.TABLE}
onClick={async () => {
@@ -179,7 +183,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.CALENDAR)}
text={t`Calendar`}
text={t(getViewTypeLabel(ViewType.CALENDAR))}
selected={currentView?.type === ViewType.CALENDAR}
focused={selectedItemId === ViewType.CALENDAR}
onClick={handleSelectCalendarViewType}
@@ -193,7 +197,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.KANBAN)}
text={t`Kanban`}
text={t(getViewTypeLabel(ViewType.KANBAN))}
disabled={isDefaultView}
focused={selectedItemId === ViewType.KANBAN}
contextualText={
@@ -96,6 +96,12 @@ describe('useComputeApplicationContentForLayoutAndLogic', () => {
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',
);
});
});
@@ -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,
@@ -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;
};
@@ -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<ViewTypeLabelKey, MessageDescriptor>;
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 },
@@ -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),
},
];