From f8c3960cf2c75f79cd80804db4a7af8334e4d10c Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 3 Apr 2026 17:27:17 +0200 Subject: [PATCH] Disable setting tab when empty (#19315) ## before image ## after image --- .../ui/layout/tab-list/components/TabList.tsx | 1 + .../layout/tab-list/types/SingleTabProps.ts | 1 + .../SettingsApplicationDetails.tsx | 11 +- .../SettingsApplicationDataTable.tsx | 38 +----- .../SettingsApplicationDataTableRow.tsx | 111 ++++-------------- .../SettingsApplicationDetailContentTab.tsx | 93 +++++---------- ...gsAvailableApplicationDetailContentTab.tsx | 28 +---- .../button/components/TabButton/TabButton.tsx | 61 ++++++---- 8 files changed, 109 insertions(+), 235 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx index e3d8d41477..92ac0bdfff 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/components/TabList.tsx @@ -173,6 +173,7 @@ export const TabList = ({ disabled={tab.disabled ?? loading} pill={tab.pill} to={behaveAsLinks ? `#${tab.id}` : undefined} + tooltipContent={tab.tooltipContent} onClick={ behaveAsLinks ? () => onChangeTab?.(tab.id) diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/SingleTabProps.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/SingleTabProps.ts index 416e53ae67..4e57937cf1 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/SingleTabProps.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/SingleTabProps.ts @@ -8,4 +8,5 @@ export type SingleTabProps = { disabled?: boolean; pill?: string | React.ReactElement; logo?: string; + tooltipContent?: string; }; diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx index 8bb79b8768..d8849c40ba 100644 --- a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx +++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx @@ -23,6 +23,7 @@ import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications import { SettingsApplicationDetailSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab'; import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab'; import { SettingsApplicationCustomTab } from '~/pages/settings/applications/tabs/SettingsApplicationCustomTab'; +import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; const APPLICATION_DETAIL_ID = 'application-detail-id'; @@ -49,11 +50,17 @@ export const SettingsApplicationDetails = () => { const settingsCustomTabFrontComponentId = application?.settingsCustomTabFrontComponentId; - const tabs = [ + const tabs: SingleTabProps[] = [ { id: 'about', title: t`About`, Icon: IconInfoCircle }, { id: 'content', title: t`Content`, Icon: IconBox }, { id: 'permissions', title: t`Permissions`, Icon: IconLock }, - { id: 'settings', title: t`Settings`, Icon: IconSettings }, + { + id: 'settings', + title: t`Settings`, + Icon: IconSettings, + tooltipContent: t`No variables to set for this application`, + disabled: (application?.applicationVariables ?? []).length === 0, + }, ...(isDefined(settingsCustomTabFrontComponentId) ? [{ id: 'custom', title: t`Custom`, Icon: IconApps }] : []), diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTable.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTable.tsx index 9d5ded2950..7f97091399 100644 --- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTable.tsx +++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTable.tsx @@ -10,23 +10,16 @@ import { H2Title } from 'twenty-ui/display'; import { SearchInput } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; + import { SettingsApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTableRow'; import { normalizeSearchText } from '~/utils/normalizeSearchText'; -export type ApplicationDataTableFieldItem = { - key: string; - label: string; - icon?: string; - type: string; -}; - export type ApplicationDataTableRow = { key: string; labelPlural: string; icon?: string; fieldsCount: number; link?: string; - fields?: ApplicationDataTableFieldItem[]; tagItem: { isCustom?: boolean; isRemote?: boolean; @@ -60,21 +53,6 @@ export const SettingsApplicationDataTable = ({ fieldGroupRows: ApplicationDataTableRow[]; }) => { const [searchTerm, setSearchTerm] = useState(''); - const [expandedRows, setExpandedRows] = useState>(new Set()); - - const toggleRow = (key: string) => { - setExpandedRows((previous) => { - const next = new Set(previous); - - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - - return next; - }); - }; const filteredObjectRows = useMemo(() => { const normalizedSearch = normalizeSearchText(searchTerm); @@ -138,24 +116,14 @@ export const SettingsApplicationDataTable = ({ {shouldDisplayObjects && ( {filteredObjectRows.map((row) => ( - toggleRow(row.key)} - /> + ))} )} {shouldDisplayFields && ( {filteredFieldGroupRows.map((row) => ( - toggleRow(row.key)} - /> + ))} )} diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTableRow.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTableRow.tsx index 136172a285..9b98901527 100644 --- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTableRow.tsx +++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTableRow.tsx @@ -1,117 +1,48 @@ import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; -import { SettingsObjectFieldDataType } from '@/settings/data-model/object-details/components/SettingsObjectFieldDataType'; import { StyledActionTableCell, StyledNameTableCell, } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents'; -import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { TableCell } from '@/ui/layout/table/components/TableCell'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { TableSubRow } from '@/ui/layout/table/components/TableSubRow'; -import { styled } from '@linaria/react'; -import { t } from '@lingui/core/macro'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; +import { IconChevronRight, useIcons } from 'twenty-ui/display'; +import { ThemeContext } from 'twenty-ui/theme-constants'; import { type ApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTable'; const MAIN_ROW_GRID_COLUMNS = '180px 1fr 98.7px 36px'; -const FIELD_SUB_ROW_GRID_COLUMNS = '180px 1fr'; - -const StyledFieldDivider = styled.div` - border-top: 1px solid ${themeCssVariables.border.color.light}; -`; export const SettingsApplicationDataTableRow = ({ row, - isExpanded, - onToggle, }: { row: ApplicationDataTableRow; - isExpanded: boolean; - onToggle: () => void; }) => { const { theme } = useContext(ThemeContext); const { getIcon } = useIcons(); const Icon = getIcon(row.icon); - const hasFields = isDefined(row.fields) && row.fields.length > 0; return ( - <> - - - {isDefined(Icon) && ( - - )} - {row.labelPlural} - - - - - {row.fieldsCount} - - {hasFields && isExpanded ? ( - - ) : ( - - )} - - - {hasFields && isExpanded && ( - <> - - - {t`Name`} - {t`Type`} - - - )} - {hasFields && - isExpanded && - row.fields?.map((field) => { - const FieldIcon = getIcon(field.icon); - - return ( - - - {isDefined(FieldIcon) && ( - - )} - {field.label} - - - - - - ); - })} - + + + {isDefined(Icon) && ( + + )} + {row.labelPlural} + + + + + {row.fieldsCount} + + + + ); }; diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailContentTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailContentTab.tsx index 5105736433..9311fe2c36 100644 --- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailContentTab.tsx +++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailContentTab.tsx @@ -12,7 +12,6 @@ import { type Application } from '~/generated-metadata/graphql'; import { SettingsAIAgentsTable } from '~/pages/settings/ai/components/SettingsAIAgentsTable'; import { SettingsApplicationDataTable, - type ApplicationDataTableFieldItem, type ApplicationDataTableRow, } from '~/pages/settings/applications/components/SettingsApplicationDataTable'; @@ -44,21 +43,11 @@ export const SettingsApplicationDetailContentTab = ({ (field) => !isHiddenSystemField(field), ); - const fields: ApplicationDataTableFieldItem[] = nonSystemFields.map( - (field) => ({ - key: field.id, - label: field.label, - icon: field.icon ?? undefined, - type: field.type, - }), - ); - return { key: objectMetadataItem.nameSingular, labelPlural: objectMetadataItem.labelPlural, icon: objectMetadataItem.icon ?? undefined, fieldsCount: nonSystemFields.length, - fields, link: getSettingsPath(SettingsPath.ObjectDetail, { objectNamePlural: objectMetadataItem.namePlural, }), @@ -78,68 +67,42 @@ export const SettingsApplicationDetailContentTab = ({ const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite']; - const fieldGroupMap = new Map< - string, - { - objectMetadataItemId: string; - fields: ApplicationDataTableFieldItem[]; - } - >(); - - for (const objectMetadataItem of objectMetadataItems) { - if (applicationObjectIds.includes(objectMetadataItem.id)) { - continue; - } - - if (FIELD_GROUP_DENY_LIST.includes(objectMetadataItem.nameSingular)) { - continue; - } - - const appFields = objectMetadataItem.fields.filter( - (field) => field.applicationId === application.id, - ); - - if (appFields.length > 0) { - fieldGroupMap.set(objectMetadataItem.id, { - objectMetadataItemId: objectMetadataItem.id, - fields: appFields.map((field) => ({ - key: field.id, - label: field.label, - icon: field.icon ?? undefined, - type: field.type, - })), - }); - } - } - - return Array.from(fieldGroupMap.values()) - .map((group) => { - const objectMetadataItem = objectMetadataItems.find( - (item) => item.id === group.objectMetadataItemId, - ); - if (!isDefined(objectMetadataItem)) { - return undefined; + return objectMetadataItems + .filter((objectMetadataItem) => { + if (applicationObjectIds.includes(objectMetadataItem.id)) { + return false; } + if (FIELD_GROUP_DENY_LIST.includes(objectMetadataItem.nameSingular)) { + return false; + } + + const appFields = objectMetadataItem.fields.filter( + (field) => field.applicationId === application.id, + ); + + return appFields.length > 0; + }) + .map((objectMetadataItem) => { + const appFieldsCount = objectMetadataItem.fields.filter( + (field) => field.applicationId === application.id, + ).length; + return { - key: objectMetadataItem.nameSingular ?? group.objectMetadataItemId, + key: objectMetadataItem.nameSingular, labelPlural: objectMetadataItem.labelPlural, icon: objectMetadataItem.icon ?? undefined, - fieldsCount: group.fields.length, - fields: group.fields, - link: isDefined(objectMetadataItem) - ? getSettingsPath(SettingsPath.ObjectDetail, { - objectNamePlural: objectMetadataItem.namePlural, - }) - : undefined, + fieldsCount: appFieldsCount, + link: getSettingsPath(SettingsPath.ObjectDetail, { + objectNamePlural: objectMetadataItem.namePlural, + }), tagItem: { - isCustom: objectMetadataItem?.isCustom, - isRemote: objectMetadataItem?.isRemote, - applicationId: objectMetadataItem?.applicationId, + isCustom: objectMetadataItem.isCustom, + isRemote: objectMetadataItem.isRemote, + applicationId: objectMetadataItem.applicationId, }, }; - }) - .filter(isDefined); + }); }, [objectMetadataItems, applicationObjectIds, application]); if (!isDefined(application)) { diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx index b11323ced8..366cde71fd 100644 --- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx +++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx @@ -6,7 +6,6 @@ import { type Manifest } from 'twenty-shared/application'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { - type ApplicationDataTableFieldItem, type ApplicationDataTableRow, SettingsApplicationDataTable, } from '~/pages/settings/applications/components/SettingsApplicationDataTable'; @@ -34,14 +33,6 @@ export const SettingsAvailableApplicationDetailContentTab = ({ labelPlural: appObject.labelPlural, icon: appObject.icon ?? undefined, fieldsCount: appObject.fields.length, - fields: appObject.fields.map( - (field): ApplicationDataTableFieldItem => ({ - key: field.name, - label: field.label, - icon: field.icon ?? undefined, - type: field.type, - }), - ), tagItem: { applicationId }, })), [objects, applicationId], @@ -56,27 +47,20 @@ export const SettingsAvailableApplicationDetailContentTab = ({ string, { objectUniversalIdentifier: string; - fieldItems: ApplicationDataTableFieldItem[]; + count: number; } >(); for (const field of fields) { const objectUid = field.objectUniversalIdentifier; - const existing = groupMap.get(objectUid); - const fieldItem: ApplicationDataTableFieldItem = { - key: field.name, - label: field.label, - icon: field.icon ?? undefined, - type: field.type, - }; if (isDefined(existing)) { - existing.fieldItems.push(fieldItem); + existing.count++; } else { groupMap.set(objectUid, { objectUniversalIdentifier: objectUid, - fieldItems: [fieldItem], + count: 1, }); } } @@ -91,8 +75,7 @@ export const SettingsAvailableApplicationDetailContentTab = ({ key: appObject.nameSingular, labelPlural: appObject.labelPlural, icon: appObject.icon ?? undefined, - fieldsCount: group.fieldItems.length, - fields: group.fieldItems, + fieldsCount: group.count, tagItem: { applicationId }, }; } @@ -117,8 +100,7 @@ export const SettingsAvailableApplicationDetailContentTab = ({ key: objectMetadataItem.nameSingular, labelPlural: objectMetadataItem.labelPlural, icon: objectMetadataItem.icon ?? undefined, - fieldsCount: group.fieldItems.length, - fields: group.fieldItems, + fieldsCount: group.count, link: getSettingsPath(SettingsPath.ObjectDetail, { objectNamePlural: objectMetadataItem.namePlural, }), diff --git a/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx index 9c2b3d4a5f..b50ed867d7 100644 --- a/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx +++ b/packages/twenty-ui/src/input/button/components/TabButton/TabButton.tsx @@ -1,4 +1,5 @@ -import { type IconComponent } from '@ui/display'; +import { styled } from '@linaria/react'; +import { AppTooltip, type IconComponent, TooltipDelay } from '@ui/display'; import { StyledTabButton } from '@ui/input/button/components/TabButton/internals/components/StyledTabBase'; import { TabContent } from '@ui/input/button/components/TabButton/internals/components/TabContent'; import { type ReactElement } from 'react'; @@ -18,8 +19,13 @@ type TabButtonProps = { pill?: string | ReactElement; contentSize?: 'sm' | 'md'; disableTestId?: boolean; + tooltipContent?: string; }; +const StyledTabTooltipWrapper = styled.div` + display: flex; +`; + export const TabButton = ({ id, active, @@ -34,28 +40,43 @@ export const TabButton = ({ pill, contentSize = 'sm', disableTestId = false, + tooltipContent, }: TabButtonProps) => { + const tabElementId = `tab-${id}`; + return ( - - + - + as={to ? Link : 'button'} + to={to} + className={className} + onClick={onClick} + > + + + {tooltipContent && ( + + )} + ); };