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:
@@ -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;
|
||||
};
|
||||
|
||||
+9
-2
@@ -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 }]
|
||||
: []),
|
||||
|
||||
+3
-35
@@ -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>
|
||||
)}
|
||||
|
||||
+21
-90
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+28
-65
@@ -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)) {
|
||||
|
||||
+5
-23
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user