Fix app design 6 (#19827)

Unify application display page and isntalled page

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-04-20 09:29:25 +02:00
committed by GitHub
parent 42f57db005
commit 5dd7eba911
30 changed files with 2154 additions and 1602 deletions
@@ -34,6 +34,12 @@ export const APPLICATION_FRAGMENT = gql`
agents {
...AgentFields
}
frontComponents {
id
name
description
applicationId
}
objects {
...ObjectMetadataFields
}
@@ -4,6 +4,7 @@ export const CUSTOM_WORKSPACE_APPLICATION_MOCK = {
id: 'dc75f982-35a2-4c1b-a63d-bd1131215377',
agents: [],
applicationVariables: [],
frontComponents: [],
availablePackages: {},
canBeUninstalled: false,
description: 'workpace custom application',
@@ -112,7 +112,12 @@ export const SettingsAdminApps = () => {
mobileGridAutoColumns={TABLE_GRID_MOBILE}
isClickable
>
<TableCell color={themeCssVariables.font.color.primary}>
<TableCell
color={themeCssVariables.font.color.primary}
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<OverflowingTextWithTooltip text={registration.name} />
</TableCell>
<TableCell overflow="hidden" align="right">
@@ -0,0 +1,232 @@
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { type ComponentType, type ReactNode } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import {
IconAlertTriangle,
IconBrandNpm,
IconLink,
IconMail,
IconWorld,
} from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export type ContentEntry = {
icon: ComponentType<{ size?: number }>;
count: number;
one: string;
many: string;
};
export type DeveloperLinks = {
websiteUrl?: string;
termsUrl?: string;
emailSupport?: string;
issueReportUrl?: string;
sourcePackageUrl?: string;
};
type SettingsApplicationAboutSidebarProps = {
actionButton?: ReactNode;
author?: string;
category?: string;
contentEntries?: ContentEntry[];
currentVersion?: string;
latestAvailableVersion?: string;
developerLinks?: DeveloperLinks;
};
const StyledSidebar = styled.div`
flex-shrink: 0;
width: 140px;
`;
const StyledSidebarSection = styled.div`
padding: ${themeCssVariables.spacing[3]} 0;
&:first-of-type {
padding-top: 0;
}
`;
const StyledSidebarLabel = styled.div`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledSidebarValue = styled.div`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledContentItem = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.sm};
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[2]};
&:last-of-type {
margin-bottom: 0;
}
`;
const StyledLink = styled.a`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.sm};
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[2]};
text-decoration: none;
&:hover {
text-decoration: underline;
}
&:last-of-type {
margin-bottom: 0;
}
`;
export const SettingsApplicationAboutSidebar = ({
actionButton,
author,
category,
contentEntries,
currentVersion,
latestAvailableVersion,
developerLinks,
}: SettingsApplicationAboutSidebarProps) => {
const isSafeUrl = (url: string | undefined): url is string => {
if (!isNonEmptyString(url)) return false;
try {
const parsed = new URL(url);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
};
const filteredContentEntries = (contentEntries ?? []).filter(
(entry) => entry.count > 0,
);
const hasDeveloperLinks =
isDefined(developerLinks) &&
(isNonEmptyString(developerLinks.websiteUrl) ||
isNonEmptyString(developerLinks.termsUrl) ||
isNonEmptyString(developerLinks.emailSupport) ||
isNonEmptyString(developerLinks.issueReportUrl) ||
isNonEmptyString(developerLinks.sourcePackageUrl));
return (
<StyledSidebar>
{isDefined(actionButton) && (
<StyledSidebarSection>{actionButton}</StyledSidebarSection>
)}
{isDefined(author) && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Created by`}</StyledSidebarLabel>
<StyledSidebarValue>{author}</StyledSidebarValue>
</StyledSidebarSection>
)}
{isDefined(category) && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Category`}</StyledSidebarLabel>
<StyledSidebarValue>{category}</StyledSidebarValue>
</StyledSidebarSection>
)}
{filteredContentEntries.length > 0 && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Content`}</StyledSidebarLabel>
{filteredContentEntries.map((entry) => (
<StyledContentItem key={entry.one}>
<entry.icon size={16} />
{entry.count} {entry.count === 1 ? entry.one : entry.many}
</StyledContentItem>
))}
</StyledSidebarSection>
)}
{isDefined(currentVersion) && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Current`}</StyledSidebarLabel>
<StyledSidebarValue>{currentVersion}</StyledSidebarValue>
</StyledSidebarSection>
)}
{isDefined(latestAvailableVersion) && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Latest`}</StyledSidebarLabel>
<StyledSidebarValue>{latestAvailableVersion}</StyledSidebarValue>
</StyledSidebarSection>
)}
{hasDeveloperLinks && (
<StyledSidebarSection>
<StyledSidebarLabel>{t`Developers links`}</StyledSidebarLabel>
{isSafeUrl(developerLinks.websiteUrl) && (
<StyledLink
href={developerLinks.websiteUrl}
target="_blank"
rel="noopener noreferrer"
>
<IconWorld size={16} />
{t`Website`}
</StyledLink>
)}
{isSafeUrl(developerLinks.termsUrl) && (
<StyledLink
href={developerLinks.termsUrl}
target="_blank"
rel="noopener noreferrer"
>
<IconLink size={16} />
{t`Terms / Privacy`}
</StyledLink>
)}
{isNonEmptyString(developerLinks.emailSupport) && (
<StyledLink
href={`mailto:${developerLinks.emailSupport}`}
target="_blank"
rel="noopener noreferrer"
>
<IconMail size={16} />
{t`Email support`}
</StyledLink>
)}
{isSafeUrl(developerLinks.issueReportUrl) && (
<StyledLink
href={developerLinks.issueReportUrl}
target="_blank"
rel="noopener noreferrer"
>
<IconAlertTriangle size={16} />
{t`Report an issue`}
</StyledLink>
)}
{isSafeUrl(developerLinks.sourcePackageUrl) && (
<StyledLink
href={developerLinks.sourcePackageUrl}
target="_blank"
rel="noopener noreferrer"
>
<IconBrandNpm size={16} />
{t`Npm package`}
</StyledLink>
)}
</StyledSidebarSection>
)}
</StyledSidebar>
);
};
@@ -0,0 +1,97 @@
import { styled } from '@linaria/react';
import { useState } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsApplicationScreenshotGalleryProps = {
screenshots: string[];
displayName: string;
};
const StyledScreenshotsContainer = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
height: 300px;
justify-content: center;
margin-bottom: ${themeCssVariables.spacing[2]};
overflow: hidden;
`;
const StyledScreenshotImage = styled.img`
height: 100%;
object-fit: contain;
width: 100%;
`;
const StyledScreenshotThumbnails = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
margin-bottom: ${themeCssVariables.spacing[6]};
`;
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
border: 1px solid
${({ isSelected }) =>
isSelected
? themeCssVariables.color.blue
: themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
cursor: pointer;
display: flex;
flex: 1;
height: 60px;
justify-content: center;
overflow: hidden;
&:hover {
border-color: ${themeCssVariables.color.blue};
}
`;
const StyledThumbnailImage = styled.img`
height: 100%;
object-fit: contain;
width: 100%;
`;
export const SettingsApplicationScreenshotGallery = ({
screenshots,
displayName,
}: SettingsApplicationScreenshotGalleryProps) => {
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
if (screenshots.length === 0) {
return null;
}
const safeIndex = Math.min(selectedScreenshotIndex, screenshots.length - 1);
return (
<>
<StyledScreenshotsContainer>
<StyledScreenshotImage
src={screenshots[safeIndex]}
alt={`${displayName} screenshot ${safeIndex + 1}`}
/>
</StyledScreenshotsContainer>
<StyledScreenshotThumbnails>
{screenshots.slice(0, 6).map((screenshot, index) => (
<StyledThumbnail
key={index}
isSelected={index === selectedScreenshotIndex}
onClick={() => setSelectedScreenshotIndex(index)}
>
<StyledThumbnailImage
src={screenshot}
alt={`${displayName} thumbnail ${index + 1}`}
/>
</StyledThumbnail>
))}
</StyledScreenshotThumbnails>
</>
);
};
@@ -0,0 +1,297 @@
import { renderHook } from '@testing-library/react';
import { useObjectAndFieldRows } from '@/settings/applications/hooks/useObjectAndFieldRows';
import { type Manifest } from 'twenty-shared/application';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
const mockObjectMetadataItems = getTestEnrichedObjectMetadataItemsMock();
const personObject = mockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const companyObject = mockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
)!;
const APP_ID = 'test-app-id';
const wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useObjectAndFieldRows', () => {
describe('with installed application', () => {
it('should return object rows for installed application objects', () => {
const installedApplication = {
id: APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
agents: [],
logicFunctions: [],
frontComponents: [],
};
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: APP_ID,
installedApplication,
}),
{ wrapper },
);
expect(result.current.objectRows).toHaveLength(1);
expect(result.current.objectRows[0].key).toBe(personObject.nameSingular);
expect(result.current.objectRows[0].labelPlural).toBe(
personObject.labelPlural,
);
expect(result.current.objectRows[0].fieldsCount).toBeGreaterThan(0);
expect(result.current.objectRows[0].link).toBeDefined();
});
it('should return empty object rows when application has no objects', () => {
const installedApplication = {
id: APP_ID,
objects: [],
name: 'Test App',
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
agents: [],
logicFunctions: [],
frontComponents: [],
};
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: APP_ID,
installedApplication,
}),
{ wrapper },
);
expect(result.current.objectRows).toHaveLength(0);
});
it('should return field group rows for fields added to other objects', () => {
const fieldBelongingToApp = companyObject.fields[0];
const installedApplication = {
id: fieldBelongingToApp.applicationId ?? APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
agents: [],
logicFunctions: [],
frontComponents: [],
};
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: installedApplication.id,
installedApplication,
}),
{ wrapper },
);
// Field group rows should not include the app's own objects
const hasOwnObject = result.current.fieldGroupRows.some(
(row) => row.key === personObject.nameSingular,
);
expect(hasOwnObject).toBe(false);
});
it('should exclude deny-listed objects from field group rows', () => {
const installedApplication = {
id: APP_ID,
objects: [],
name: 'Test App',
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
agents: [],
logicFunctions: [],
frontComponents: [],
};
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: APP_ID,
installedApplication,
}),
{ wrapper },
);
const hasDeniedObject = result.current.fieldGroupRows.some(
(row) => row.key === 'timelineActivity' || row.key === 'favorite',
);
expect(hasDeniedObject).toBe(false);
});
});
describe('with manifest content', () => {
it('should return object rows from manifest objects', () => {
const manifestContent = {
objects: [
{
universalIdentifier: 'uid-1',
nameSingular: 'customObject',
namePlural: 'customObjects',
labelSingular: 'Custom Object',
labelPlural: 'Custom Objects',
icon: 'IconBox',
fields: [{ name: 'field1' }, { name: 'field2' }],
},
],
fields: [],
} as unknown as Manifest;
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: 'app-uid',
manifestContent,
}),
{ wrapper },
);
expect(result.current.objectRows).toHaveLength(1);
expect(result.current.objectRows[0].key).toBe('customObject');
expect(result.current.objectRows[0].labelPlural).toBe('Custom Objects');
expect(result.current.objectRows[0].fieldsCount).toBe(2);
expect(result.current.objectRows[0].tagItem.applicationId).toBe(
'app-uid',
);
});
it('should return field group rows grouped by object from manifest fields', () => {
const manifestContent = {
objects: [
{
universalIdentifier: 'custom-obj-uid',
nameSingular: 'customObj',
namePlural: 'customObjs',
labelSingular: 'Custom',
labelPlural: 'Customs',
icon: 'IconBox',
fields: [],
},
],
fields: [
{
objectUniversalIdentifier: 'custom-obj-uid',
name: 'field1',
},
{
objectUniversalIdentifier: 'custom-obj-uid',
name: 'field2',
},
{
objectUniversalIdentifier: 'custom-obj-uid',
name: 'field3',
},
],
} as unknown as Manifest;
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: 'app-uid',
manifestContent,
}),
{ wrapper },
);
expect(result.current.fieldGroupRows).toHaveLength(1);
expect(result.current.fieldGroupRows[0].key).toBe('customObj');
expect(result.current.fieldGroupRows[0].fieldsCount).toBe(3);
});
it('should return empty field group rows when manifest has no fields', () => {
const manifestContent = {
objects: [],
fields: [],
} as unknown as Manifest;
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: 'app-uid',
manifestContent,
}),
{ wrapper },
);
expect(result.current.fieldGroupRows).toHaveLength(0);
});
it('should return empty rows when no data is provided', () => {
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: 'app-uid',
}),
{ wrapper },
);
expect(result.current.objectRows).toHaveLength(0);
expect(result.current.fieldGroupRows).toHaveLength(0);
});
});
describe('data source priority', () => {
it('should use installed application data when both sources are provided', () => {
const installedApplication = {
id: APP_ID,
objects: [{ id: personObject.id }],
name: 'Test App',
canBeUninstalled: true,
availablePackages: {},
applicationVariables: [],
agents: [],
logicFunctions: [],
frontComponents: [],
};
const manifestContent = {
objects: [
{
universalIdentifier: 'uid-1',
nameSingular: 'manifestObj',
namePlural: 'manifestObjs',
labelSingular: 'Manifest',
labelPlural: 'Manifests',
icon: 'IconBox',
fields: [],
},
],
fields: [],
} as unknown as Manifest;
const { result } = renderHook(
() =>
useObjectAndFieldRows({
applicationId: APP_ID,
installedApplication,
manifestContent,
}),
{ wrapper },
);
// Should use installed data, not manifest
expect(result.current.objectRows[0].key).toBe(personObject.nameSingular);
expect(
result.current.objectRows.some((r) => r.key === 'manifestObj'),
).toBe(false);
});
});
});
@@ -0,0 +1,179 @@
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useMemo } from 'react';
import { type Manifest } from 'twenty-shared/application';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { type Application } from '~/generated-metadata/graphql';
import { type ApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTable';
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
type InstalledApplicationForObjectRows = Omit<
Application,
'objects' | 'universalIdentifier' | 'frontComponents'
> & {
objects: { id: string }[];
};
export const useObjectAndFieldRows = ({
applicationId,
installedApplication,
manifestContent,
}: {
applicationId: string;
installedApplication?: InstalledApplicationForObjectRows;
manifestContent?: Manifest;
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const installedObjectIds = useMemo(
() => installedApplication?.objects.map((object) => object.id) ?? [],
[installedApplication?.objects],
);
const objectRows = useMemo((): ApplicationDataTableRow[] => {
if (isDefined(installedApplication)) {
if (installedApplication.objects.length === 0) {
return [];
}
return objectMetadataItems
.filter((item) => installedObjectIds.includes(item.id))
.map((item) => ({
key: item.nameSingular,
labelPlural: item.labelPlural,
icon: item.icon ?? undefined,
fieldsCount: item.fields.filter((f) => !isHiddenSystemField(f))
.length,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: item.namePlural,
}),
tagItem: {
isCustom: item.isCustom,
isRemote: item.isRemote,
applicationId: item.applicationId,
},
}));
}
return (manifestContent?.objects ?? []).map((appObject) => ({
key: appObject.nameSingular,
labelPlural: appObject.labelPlural,
icon: appObject.icon ?? undefined,
fieldsCount: appObject.fields.length,
tagItem: { applicationId },
}));
}, [
installedApplication,
manifestContent?.objects,
objectMetadataItems,
installedObjectIds,
applicationId,
]);
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
if (isDefined(installedApplication)) {
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
return objectMetadataItems
.filter((item) => {
if (installedObjectIds.includes(item.id)) return false;
if (FIELD_GROUP_DENY_LIST.includes(item.nameSingular)) return false;
return item.fields.some(
(field) => field.applicationId === installedApplication.id,
);
})
.map((item) => ({
key: item.nameSingular,
labelPlural: item.labelPlural,
icon: item.icon ?? undefined,
fieldsCount: item.fields.filter(
(field) => field.applicationId === installedApplication.id,
).length,
link: getSettingsPath(SettingsPath.ObjectDetail, {
objectNamePlural: item.namePlural,
}),
tagItem: {
isCustom: item.isCustom,
isRemote: item.isRemote,
applicationId: item.applicationId,
},
}));
}
const manifestFields = manifestContent?.fields ?? [];
const manifestObjects = manifestContent?.objects ?? [];
if (manifestFields.length === 0) return [];
const groupMap = new Map<
string,
{ objectUniversalIdentifier: string; count: number }
>();
for (const field of manifestFields) {
const objectUid = field.objectUniversalIdentifier;
const existing = groupMap.get(objectUid);
if (isDefined(existing)) {
existing.count++;
} else {
groupMap.set(objectUid, {
objectUniversalIdentifier: objectUid,
count: 1,
});
}
}
return Array.from(groupMap.values())
.map((group) => {
const appObject = manifestObjects.find(
(obj) => obj.universalIdentifier === group.objectUniversalIdentifier,
);
if (isDefined(appObject)) {
return {
key: appObject.nameSingular,
labelPlural: appObject.labelPlural,
icon: appObject.icon ?? undefined,
fieldsCount: group.count,
tagItem: { applicationId },
};
}
const standardObjectName = findObjectNameByUniversalIdentifier(
group.objectUniversalIdentifier,
);
const objectMetadataItem = isDefined(standardObjectName)
? objectMetadataItems.find(
(item) => item.nameSingular === standardObjectName,
)
: undefined;
if (!isDefined(objectMetadataItem)) {
return;
}
return {
key: objectMetadataItem.nameSingular,
labelPlural: objectMetadataItem.labelPlural,
icon: objectMetadataItem.icon ?? undefined,
fieldsCount: group.count,
tagItem: {},
};
})
.filter(isDefined);
}, [
installedApplication,
manifestContent?.fields,
manifestContent?.objects,
objectMetadataItems,
installedObjectIds,
applicationId,
]);
return { objectRows, fieldGroupRows };
};
@@ -1,13 +1,15 @@
import { styled } from '@linaria/react';
import { type LogicFunction } from '~/generated-metadata/graphql';
import {
type LogicFunctionTableRow,
StyledTableRow,
} from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import {
IconChevronRight,
IconCode,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { StyledTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
import { useContext } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledIconContainer = styled.span`
@@ -21,43 +23,13 @@ const StyledIconChevronRightContainer = styled(StyledIconContainer)`
export const SettingsLogicFunctionsFieldItemTableRow = ({
logicFunction,
to,
}: {
logicFunction: LogicFunction;
to: string;
logicFunction: LogicFunctionTableRow;
}) => {
const { theme } = useContext(ThemeContext);
const computeTrigger = () => {
const cronTrigger = logicFunction.cronTriggerSettings;
const routeTrigger = logicFunction.httpRouteTriggerSettings;
const databaseEventTriggerSettings =
logicFunction.databaseEventTriggerSettings;
const isTool = logicFunction.isTool;
if (isTool) {
return 'Tool';
}
if (cronTrigger) {
return 'Cron';
}
if (routeTrigger) {
return 'Route';
}
if (databaseEventTriggerSettings) {
return databaseEventTriggerSettings.eventName;
}
return '';
};
return (
<StyledTableRow to={to}>
<StyledTableRow to={logicFunction.link}>
<TableCell
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
@@ -74,18 +46,20 @@ export const SettingsLogicFunctionsFieldItemTableRow = ({
whiteSpace="nowrap"
overflow="hidden"
>
<OverflowingTextWithTooltip text={computeTrigger()} />
<OverflowingTextWithTooltip text={logicFunction.trigger} />
</TableCell>
<TableCell
align="center"
padding={`0 ${themeCssVariables.spacing[1]} 0 ${themeCssVariables.spacing[2]}`}
>
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconChevronRightContainer>
{logicFunction.link && (
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconChevronRightContainer>
)}
</TableCell>
</StyledTableRow>
);
@@ -4,14 +4,17 @@ import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { styled } from '@linaria/react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { type LogicFunction } from '~/generated-metadata/graphql';
import { useLingui } from '@lingui/react/macro';
import React from 'react';
import { useParams } from 'react-router-dom';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export type LogicFunctionTableRow = {
key: string;
name: string;
trigger: string;
link?: string;
};
export const StyledTableRow = (
props: React.ComponentProps<typeof TableRow>,
) => (
@@ -29,10 +32,8 @@ const StyledTableBodyContainer = styled.div`
export const SettingsLogicFunctionsTable = ({
logicFunctions,
}: {
logicFunctions: LogicFunction[];
logicFunctions: LogicFunctionTableRow[];
}) => {
const { applicationId = '' } = useParams();
const { t } = useLingui();
if (logicFunctions.length === 0) {
@@ -48,14 +49,10 @@ export const SettingsLogicFunctionsTable = ({
</StyledTableRow>
<StyledTableBodyContainer>
<TableBody>
{logicFunctions.map((logicFunction: LogicFunction) => (
{logicFunctions.map((logicFunction) => (
<SettingsLogicFunctionsFieldItemTableRow
key={logicFunction.id}
key={logicFunction.key}
logicFunction={logicFunction}
to={getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: logicFunction.id,
})}
/>
))}
</TableBody>