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`. | |  |  | ## 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:
committed by
GitHub
parent
40e386bfb4
commit
de9af38a67
+7
-3
@@ -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
|
||||
/>
|
||||
|
||||
+8
-4
@@ -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={
|
||||
|
||||
+11
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
+15
-15
@@ -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 },
|
||||
|
||||
+8
-5
@@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user