Disable setting tab when empty (#19315)

## before
<img width="1063" height="520" alt="image"
src="https://github.com/user-attachments/assets/db92432b-a30c-49e8-9246-a09d0815e6c9"
/>


## after
<img width="1049" height="490" alt="image"
src="https://github.com/user-attachments/assets/e926b065-6a65-417b-b802-0e1df6ca638f"
/>
This commit is contained in:
martmull
2026-04-03 17:27:17 +02:00
committed by GitHub
parent d562a384c2
commit f8c3960cf2
8 changed files with 109 additions and 235 deletions
@@ -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)
@@ -8,4 +8,5 @@ export type SingleTabProps<T extends string = string> = {
disabled?: boolean;
pill?: string | React.ReactElement;
logo?: string;
tooltipContent?: string;
};
@@ -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 }]
: []),
@@ -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<Set<string>>(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 && (
<TableSection title={t`Objects`}>
{filteredObjectRows.map((row) => (
<SettingsApplicationDataTableRow
key={row.key}
row={row}
isExpanded={expandedRows.has(row.key)}
onToggle={() => toggleRow(row.key)}
/>
<SettingsApplicationDataTableRow key={row.key} row={row} />
))}
</TableSection>
)}
{shouldDisplayFields && (
<TableSection title={t`Fields`}>
{filteredFieldGroupRows.map((row) => (
<SettingsApplicationDataTableRow
key={row.key}
row={row}
isExpanded={expandedRows.has(row.key)}
onToggle={() => toggleRow(row.key)}
/>
<SettingsApplicationDataTableRow key={row.key} row={row} />
))}
</TableSection>
)}
@@ -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 (
<>
<TableRow
gridAutoColumns={MAIN_ROW_GRID_COLUMNS}
onClick={hasFields ? onToggle : undefined}
to={hasFields ? undefined : row.link}
>
<StyledNameTableCell>
{isDefined(Icon) && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{row.labelPlural}
</StyledNameTableCell>
<TableCell>
<SettingsItemTypeTag item={row.tagItem} />
</TableCell>
<TableCell align="right">{row.fieldsCount}</TableCell>
<StyledActionTableCell>
{hasFields && isExpanded ? (
<IconChevronDown
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledActionTableCell>
</TableRow>
{hasFields && isExpanded && (
<>
<StyledFieldDivider />
<TableSubRow gridAutoColumns={FIELD_SUB_ROW_GRID_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Type`}</TableHeader>
</TableSubRow>
</>
)}
{hasFields &&
isExpanded &&
row.fields?.map((field) => {
const FieldIcon = getIcon(field.icon);
return (
<TableSubRow
key={field.key}
gridAutoColumns={FIELD_SUB_ROW_GRID_COLUMNS}
>
<TableCell
color={themeCssVariables.font.color.secondary}
gap={themeCssVariables.spacing[2]}
>
{isDefined(FieldIcon) && (
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
{field.label}
</TableCell>
<TableCell>
<SettingsObjectFieldDataType
value={field.type as SettingsFieldType}
/>
</TableCell>
</TableSubRow>
);
})}
</>
<TableRow gridAutoColumns={MAIN_ROW_GRID_COLUMNS} to={row.link}>
<StyledNameTableCell>
{isDefined(Icon) && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{row.labelPlural}
</StyledNameTableCell>
<TableCell>
<SettingsItemTypeTag item={row.tagItem} />
</TableCell>
<TableCell align="right">{row.fieldsCount}</TableCell>
<StyledActionTableCell>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
</StyledActionTableCell>
</TableRow>
);
};
@@ -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)) {
@@ -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,
}),
@@ -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 (
<StyledTabButton
data-testid={disableTestId ? undefined : `tab-${id}`}
active={active}
disabled={disabled}
as={to ? Link : 'button'}
to={to}
className={className}
onClick={onClick}
>
<TabContent
id={id}
<StyledTabTooltipWrapper key={id} id={tabElementId}>
<StyledTabButton
data-testid={disableTestId ? undefined : `tab-${id}`}
active={active}
disabled={disabled}
LeftIcon={LeftIcon}
title={title}
logo={logo}
RightIcon={RightIcon}
pill={pill}
contentSize={contentSize}
/>
</StyledTabButton>
as={to ? Link : 'button'}
to={to}
className={className}
onClick={onClick}
>
<TabContent
id={id}
active={active}
disabled={disabled}
LeftIcon={LeftIcon}
title={title}
logo={logo}
RightIcon={RightIcon}
pill={pill}
contentSize={contentSize}
/>
</StyledTabButton>
{tooltipContent && (
<AppTooltip
anchorSelect={`#${tabElementId}`}
content={tooltipContent}
noArrow
place="bottom"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
)}
</StyledTabTooltipWrapper>
);
};