Skip to content

Commit 8f36218

Browse files
FelixMalfaitclaudeclaude[bot]
authored
Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary Iterative redesign of two related areas in settings, plus a new `pages/settings/layout/` folder for read-only entity detail pages. ### Application content tab - **Grouped into three sections** — Data / Layout / Logic — each with one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the role-permissions pattern). Replaces six per-category table/row components with one uniform `<SettingsApplicationContentSubtable>` + `ApplicationContentRow` shape (net **−~700 lines** across the refactor). - **All 10 row categories now clickable** for installed apps: - Objects / Fields / Logic functions / Front components → existing detail pages - Agents → existing `AiAgentDetail` - Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId + name`) - Roles → existing `RoleDetail` (looked up by `Role.universalIdentifier`) - Views / Page layouts / Navigation menu items → **new** detail pages (see below) - **Lifecycle hooks visible** — `pre-install` / `post-install` logic functions are surfaced in the Trigger column instead of appearing as empty/misconfigured. ### Logic function settings (Triggers + Test tabs) - Triggers tab is now editable (HTTP / Cron / Database event / AI tool) with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the toggle, header, and read-only short-circuit. - HTTP section gets a Live URL field with copy-to-clipboard. - Each section shows a **Sample input** preview (the JSON the function will receive) using the same payload builders the Test tab uses. - Test tab: **Simulate trigger** buttons that prefill the JSON input from the configured trigger's schema. Replaces an unclickable `<Select>` (which auto-disables when there's only one option — the typical case). - Read-only behavior for installed-app functions: explicit `<Callout>` notice when there's no trigger; trigger sections render as disabled controls when there is one. - Removed the empty Environment Variables section from the Settings tab (it just told the user to go elsewhere). ### New `pages/settings/layout/` folder Three new app-scoped detail pages so users can drill into entities the GraphQL `Application` type doesn't expose by id (keyed by manifest `universalIdentifier`): - `ApplicationViewDetail` — type, object, visibility + Fields / Filters / Sorts subsections (field UIDs resolved to readable labels via `useFieldLabelByUid`) - `ApplicationPageLayoutDetail` — type, object + per-tab subsections listing widgets - `ApplicationNavigationMenuItemDetail` — type, destination (resolved), icon, color, position Each page reads from the marketplace manifest the parent app page already loads (no extra queries). Folder set up so a future "Layout" settings tab can grow here (analogous to the existing `data-model/` folder under the Data tab). ### Other consistency fixes - Breadcrumbs on every app-scoped entity detail page now include a category crumb so users know what they're looking at: `Workspace / Applications / Timely / Navigation menu items / Time entry`. - Title fallback for nav menu items uses the resolved destination (`"Time entry"`) instead of the raw enum (`"OBJECT"`). - New shared utils: `getNavigationMenuItemDestination`, `resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`, `<MonoText>`. ## Backend changes Only one minor schema-shape change (additive): added `applicationId` to the `SkillFields` GraphQL fragment and `universalIdentifier` to the `RoleFragment` so the new lookups have what they need. Generated metadata schema patched in-tree to match — regenerate with `nx run twenty-front:graphql:generate --configuration=metadata` if it drifts. ## Test plan - [ ] Application content tab on an installed app shows the 3 grouped sections; rows in each section are clickable - [ ] Click an Object → existing object detail page - [ ] Click a Field → existing field-edit page - [ ] Click an Agent / Skill / Role → existing detail page - [ ] Click a View / Page layout / Navigation menu item → new read-only detail page; subsections (Fields/Filters/Sorts for views, per-tab widgets for page layouts) populate correctly - [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in `<Category> / <Entity name>` - [ ] Logic function Triggers tab: toggle each trigger type on/off, see the Sample input preview update; for installed apps, sections render as read-only - [ ] Test tab: each "Simulate trigger" button prefills the JSON editor with the matching payload shape - [ ] Functions list: a function configured as `post-install` shows "Post-install" in the Trigger column 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
1 parent 8998009 commit 8f36218

52 files changed

Lines changed: 2772 additions & 974 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/twenty-front/jest.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ const jestConfig = {
6161
extensionsToTreatAsEsm: ['.ts', '.tsx'],
6262
coverageThreshold: {
6363
global: {
64-
statements: 47.9,
65-
lines: 46,
64+
statements: 47.3,
65+
lines: 45.9,
6666
functions: 39.5,
6767
},
6868
},

packages/twenty-front/src/generated-metadata/graphql.ts

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,28 @@ const SettingsApplicationDetails = lazy(() =>
180180
),
181181
);
182182

183+
const SettingsApplicationFrontComponentDetail = lazy(() =>
184+
import(
185+
'~/pages/settings/applications/SettingsApplicationFrontComponentDetail'
186+
).then((module) => ({
187+
default: module.SettingsApplicationFrontComponentDetail,
188+
})),
189+
);
190+
191+
const SettingsLayoutViewDetail = lazy(() =>
192+
import('~/pages/settings/layout/SettingsLayoutViewDetail').then((module) => ({
193+
default: module.SettingsLayoutViewDetail,
194+
})),
195+
);
196+
197+
const SettingsLayoutPageLayoutDetail = lazy(() =>
198+
import('~/pages/settings/layout/SettingsLayoutPageLayoutDetail').then(
199+
(module) => ({
200+
default: module.SettingsLayoutPageLayoutDetail,
201+
}),
202+
),
203+
);
204+
183205
const SettingsAdminApplicationRegistrationDetail = lazy(() =>
184206
import(
185207
'~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail'
@@ -752,6 +774,18 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
752774
path={SettingsPath.ApplicationLogicFunctionDetail}
753775
element={<SettingsLogicFunctionDetail />}
754776
/>
777+
<Route
778+
path={SettingsPath.ApplicationFrontComponentDetail}
779+
element={<SettingsApplicationFrontComponentDetail />}
780+
/>
781+
<Route
782+
path={SettingsPath.ApplicationViewDetail}
783+
element={<SettingsLayoutViewDetail />}
784+
/>
785+
<Route
786+
path={SettingsPath.ApplicationPageLayoutDetail}
787+
element={<SettingsLayoutPageLayoutDetail />}
788+
/>
755789
<Route
756790
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
757791
element={<SettingsApplicationRegistrationConfigVariableDetail />}

packages/twenty-front/src/modules/applications/graphql/fragments/applicationFragment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export const APPLICATION_FRAGMENT = gql`
3939
name
4040
description
4141
applicationId
42+
componentName
43+
builtComponentChecksum
44+
universalIdentifier
45+
isHeadless
46+
usesSdkClient
47+
createdAt
48+
updatedAt
4249
}
4350
objects {
4451
...ObjectMetadataFields

packages/twenty-front/src/modules/logic-functions/graphql/fragments/logicFunctionFragment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const LOGIC_FUNCTION_FRAGMENT = gql`
1515
databaseEventTriggerSettings
1616
httpRouteTriggerSettings
1717
applicationId
18+
universalIdentifier
1819
createdAt
1920
updatedAt
2021
}

packages/twenty-front/src/modules/logic-functions/hooks/__tests__/useLogicFunctionUpdateFormState.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ describe('useLogicFunctionUpdateFormState', () => {
4646
properties: {},
4747
type: 'object',
4848
},
49+
cronTriggerSettings: null,
50+
databaseEventTriggerSettings: null,
51+
httpRouteTriggerSettings: null,
4952
});
5053
});
5154
});

packages/twenty-front/src/modules/logic-functions/hooks/useLogicFunctionUpdateFormState.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
22
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
33
import { isDefined } from 'twenty-shared/utils';
4+
import {
5+
type CronTriggerSettings,
6+
type DatabaseEventTriggerSettings,
7+
type HttpRouteTriggerSettings,
8+
} from 'twenty-shared/application';
49
import { type LogicFunction } from '~/generated-metadata/graphql';
510
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
611
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
@@ -12,6 +17,9 @@ export type LogicFunctionFormValues = {
1217
timeoutSeconds: number;
1318
sourceHandlerCode: string;
1419
toolInputSchema?: object;
20+
cronTriggerSettings: CronTriggerSettings | null;
21+
databaseEventTriggerSettings: DatabaseEventTriggerSettings | null;
22+
httpRouteTriggerSettings: HttpRouteTriggerSettings | null;
1523
};
1624

1725
type SetLogicFunctionFormValues = Dispatch<
@@ -35,6 +43,9 @@ export const useLogicFunctionUpdateFormState = ({
3543
sourceHandlerCode: '',
3644
timeoutSeconds: 300,
3745
toolInputSchema: DEFAULT_TOOL_INPUT_SCHEMA,
46+
cronTriggerSettings: null,
47+
databaseEventTriggerSettings: null,
48+
httpRouteTriggerSettings: null,
3849
});
3950

4051
const { sourceHandlerCode, loading: logicFunctionSourceCodeLoading } =
@@ -57,6 +68,11 @@ export const useLogicFunctionUpdateFormState = ({
5768
timeoutSeconds: logicFunction.timeoutSeconds ?? 300,
5869
toolInputSchema:
5970
logicFunction.toolInputSchema || DEFAULT_TOOL_INPUT_SCHEMA,
71+
cronTriggerSettings: logicFunction.cronTriggerSettings ?? null,
72+
databaseEventTriggerSettings:
73+
logicFunction.databaseEventTriggerSettings ?? null,
74+
httpRouteTriggerSettings:
75+
logicFunction.httpRouteTriggerSettings ?? null,
6076
}));
6177
}
6278
}, [logicFunction]);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getLogicFunctionTriggerLabel } from '@/logic-functions/utils/getLogicFunctionTriggerLabel';
2+
3+
describe('getLogicFunctionTriggerLabel', () => {
4+
it('returns Post-install when the function matches the post-install identifier', () => {
5+
expect(
6+
getLogicFunctionTriggerLabel(
7+
{ universalIdentifier: 'uid-post' },
8+
{ postInstallUniversalIdentifier: 'uid-post' },
9+
),
10+
).toBe('Post-install');
11+
});
12+
13+
it('returns Pre-install when the function matches the pre-install identifier', () => {
14+
expect(
15+
getLogicFunctionTriggerLabel(
16+
{ universalIdentifier: 'uid-pre' },
17+
{ preInstallUniversalIdentifier: 'uid-pre' },
18+
),
19+
).toBe('Pre-install');
20+
});
21+
22+
it('does not match when both identifiers are undefined', () => {
23+
expect(getLogicFunctionTriggerLabel({}, {})).toBe('');
24+
});
25+
26+
it('returns AI tool when isTool is set', () => {
27+
expect(getLogicFunctionTriggerLabel({ isTool: true })).toBe('AI tool');
28+
});
29+
30+
it('returns Cron when cron settings are present', () => {
31+
expect(getLogicFunctionTriggerLabel({ cronTriggerSettings: {} })).toBe(
32+
'Cron',
33+
);
34+
});
35+
36+
it('returns HTTP when http settings are present', () => {
37+
expect(getLogicFunctionTriggerLabel({ httpRouteTriggerSettings: {} })).toBe(
38+
'HTTP',
39+
);
40+
});
41+
42+
it('returns the database event name when it exists', () => {
43+
expect(
44+
getLogicFunctionTriggerLabel({
45+
databaseEventTriggerSettings: { eventName: 'person.created' },
46+
}),
47+
).toBe('person.created');
48+
});
49+
50+
it('falls back to a generic label when the database event name is missing', () => {
51+
expect(
52+
getLogicFunctionTriggerLabel({ databaseEventTriggerSettings: {} }),
53+
).toBe('Database event');
54+
});
55+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { t } from '@lingui/core/macro';
2+
import { isDefined } from 'twenty-shared/utils';
3+
4+
type LogicFunctionLike = {
5+
universalIdentifier?: string | null;
6+
isTool?: boolean;
7+
cronTriggerSettings?: unknown;
8+
httpRouteTriggerSettings?: unknown;
9+
databaseEventTriggerSettings?: { eventName?: string } | null;
10+
};
11+
12+
export const getLogicFunctionTriggerLabel = (
13+
lf: LogicFunctionLike,
14+
options: {
15+
postInstallUniversalIdentifier?: string;
16+
preInstallUniversalIdentifier?: string;
17+
} = {},
18+
): string => {
19+
if (
20+
isDefined(lf.universalIdentifier) &&
21+
lf.universalIdentifier === options.postInstallUniversalIdentifier
22+
) {
23+
return t`Post-install`;
24+
}
25+
if (
26+
isDefined(lf.universalIdentifier) &&
27+
lf.universalIdentifier === options.preInstallUniversalIdentifier
28+
) {
29+
return t`Pre-install`;
30+
}
31+
if (lf.isTool) return t`AI tool`;
32+
if (lf.cronTriggerSettings) return t`Cron`;
33+
if (lf.httpRouteTriggerSettings) return t`HTTP`;
34+
if (lf.databaseEventTriggerSettings) {
35+
return lf.databaseEventTriggerSettings.eventName ?? t`Database event`;
36+
}
37+
return '';
38+
};

packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/__tests__/__snapshots__/generateDepthRecordGqlFieldsFromObject.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record gql fields from object 1`] = `
44
{

0 commit comments

Comments
 (0)