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>
This commit is contained in:
+1
-12
@@ -1,11 +1,10 @@
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, IconClockHour8, IconTool } from 'twenty-ui/display';
|
||||
import { H2Title, IconClockHour8 } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
@@ -53,16 +52,6 @@ export const SettingsLogicFunctionNewForm = ({
|
||||
onChange={onChange('description')}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconTool}
|
||||
title={t`Available as tool`}
|
||||
description={t`When enabled, AI agents and workflow automations can discover and call this function`}
|
||||
checked={formValues.isTool}
|
||||
onChange={onChange('isTool')}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</Card>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentCounter
|
||||
Icon={IconClockHour8}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { LinkChip } from 'twenty-ui/components';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
|
||||
const { applicationId = '' } = useParams<{ applicationId: string }>();
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Environment Variables`}
|
||||
description={t`Accessible in your function via process.env.KEY`}
|
||||
/>
|
||||
<Trans>
|
||||
Environment variables are defined at application level for all
|
||||
functions. Please check{' '}
|
||||
<LinkChip
|
||||
label={t`application detail page`}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'settings',
|
||||
)}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
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 { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledIconContainer = styled.span`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledIconChevronRightContainer = styled(StyledIconContainer)`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionsFieldItemTableRow = ({
|
||||
logicFunction,
|
||||
}: {
|
||||
logicFunction: LogicFunctionTableRow;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledTableRow to={logicFunction.link}>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
>
|
||||
<StyledIconContainer>
|
||||
<IconCode size={theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={logicFunction.name} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.secondary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
align={'right'}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={logicFunction.trigger} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="center"
|
||||
padding={`0 ${themeCssVariables.spacing[1]} 0 ${themeCssVariables.spacing[2]}`}
|
||||
>
|
||||
{logicFunction.link && (
|
||||
<StyledIconChevronRightContainer>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconChevronRightContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { SettingsLogicFunctionsFieldItemTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsFieldItemTableRow';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
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 { useLingui } from '@lingui/react/macro';
|
||||
import React from 'react';
|
||||
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>,
|
||||
) => (
|
||||
<TableRow
|
||||
gridTemplateColumns="300px 1fr 32px"
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const StyledTableBodyContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionsTable = ({
|
||||
logicFunctions,
|
||||
}: {
|
||||
logicFunctions: LogicFunctionTableRow[];
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
if (logicFunctions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align={'right'}>{t`Trigger`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{logicFunctions.map((logicFunction) => (
|
||||
<SettingsLogicFunctionsFieldItemTableRow
|
||||
key={logicFunction.key}
|
||||
logicFunction={logicFunction}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBodyContainer>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
+5
-9
@@ -1,5 +1,4 @@
|
||||
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
|
||||
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
|
||||
export const SettingsLogicFunctionSettingsTab = ({
|
||||
@@ -14,13 +13,10 @@ export const SettingsLogicFunctionSettingsTab = ({
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+102
-3
@@ -1,12 +1,34 @@
|
||||
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
|
||||
import { LogicFunctionLogs } from '@/logic-functions/components/LogicFunctionLogs';
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
|
||||
import {
|
||||
buildDatabaseEventPayload,
|
||||
buildHttpPayload,
|
||||
buildToolPayloadFromSchema,
|
||||
type TriggerKind,
|
||||
} from '@/settings/logic-functions/utils/getTriggerSamplePayload';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
H2Title,
|
||||
IconClock,
|
||||
IconDatabase,
|
||||
IconPlayerPlay,
|
||||
IconTool,
|
||||
IconWebhook,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogicFunction';
|
||||
|
||||
type TriggerButton = {
|
||||
kind: TriggerKind;
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -19,13 +41,27 @@ const StyledCodeEditorContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTriggerButtonRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledTriggerLabel = styled.span`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTestTab = ({
|
||||
handleExecute,
|
||||
logicFunctionId,
|
||||
formValues,
|
||||
isTesting = false,
|
||||
}: {
|
||||
handleExecute: () => void;
|
||||
logicFunctionId: string;
|
||||
formValues: LogicFunctionFormValues;
|
||||
isTesting?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
@@ -35,6 +71,32 @@ export const SettingsLogicFunctionTestTab = ({
|
||||
logicFunctionId,
|
||||
});
|
||||
|
||||
const {
|
||||
httpRouteTriggerSettings,
|
||||
cronTriggerSettings,
|
||||
databaseEventTriggerSettings,
|
||||
toolInputSchema,
|
||||
isTool,
|
||||
} = formValues;
|
||||
|
||||
const triggerButtons: TriggerButton[] = [];
|
||||
if (isDefined(httpRouteTriggerSettings)) {
|
||||
triggerButtons.push({ kind: 'http', label: t`HTTP`, Icon: IconWebhook });
|
||||
}
|
||||
if (isDefined(cronTriggerSettings)) {
|
||||
triggerButtons.push({ kind: 'cron', label: t`Cron`, Icon: IconClock });
|
||||
}
|
||||
if (isDefined(databaseEventTriggerSettings)) {
|
||||
triggerButtons.push({
|
||||
kind: 'databaseEvent',
|
||||
label: t`Database event`,
|
||||
Icon: IconDatabase,
|
||||
});
|
||||
}
|
||||
if (isTool) {
|
||||
triggerButtons.push({ kind: 'tool', label: t`AI tool`, Icon: IconTool });
|
||||
}
|
||||
|
||||
const onChange = (value: string) => {
|
||||
try {
|
||||
updateLogicFunctionInput(JSON.parse(value));
|
||||
@@ -43,13 +105,50 @@ export const SettingsLogicFunctionTestTab = ({
|
||||
}
|
||||
};
|
||||
|
||||
const fillSamplePayload = (kind: TriggerKind) => {
|
||||
const payload = (() => {
|
||||
switch (kind) {
|
||||
case 'http':
|
||||
return isDefined(httpRouteTriggerSettings)
|
||||
? buildHttpPayload(httpRouteTriggerSettings)
|
||||
: {};
|
||||
case 'cron':
|
||||
return {};
|
||||
case 'databaseEvent':
|
||||
return isDefined(databaseEventTriggerSettings)
|
||||
? buildDatabaseEventPayload(databaseEventTriggerSettings)
|
||||
: {};
|
||||
case 'tool':
|
||||
return buildToolPayloadFromSchema(toolInputSchema);
|
||||
}
|
||||
})();
|
||||
updateLogicFunctionInput(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Test your function`}
|
||||
description={t`Insert a JSON input, then press "Run" to test your function.`}
|
||||
description={t`Insert a JSON input, then press "Run Function".`}
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
{triggerButtons.length > 0 && (
|
||||
<div>
|
||||
<StyledTriggerLabel>{t`Fill with sample input from`}</StyledTriggerLabel>
|
||||
<StyledTriggerButtonRow>
|
||||
{triggerButtons.map((trigger) => (
|
||||
<Button
|
||||
key={trigger.kind}
|
||||
Icon={trigger.Icon}
|
||||
title={trigger.label}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => fillSamplePayload(trigger.kind)}
|
||||
/>
|
||||
))}
|
||||
</StyledTriggerButtonRow>
|
||||
</div>
|
||||
)}
|
||||
<StyledCodeEditorContainer>
|
||||
<CoreEditorHeader
|
||||
title={t`Input`}
|
||||
|
||||
+68
-130
@@ -1,155 +1,93 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
import { SettingsLogicFunctionCronTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionCronTriggerSection';
|
||||
import { SettingsLogicFunctionDatabaseEventTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionDatabaseEventTriggerSection';
|
||||
import { SettingsLogicFunctionHttpTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionHttpTriggerSection';
|
||||
import { SettingsLogicFunctionToolTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionToolTriggerSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { Callout, IconInfoCircle } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const ROUTE_TRIGGER_GRID_TEMPLATE_COLUMNS = '1fr 120px 120px';
|
||||
|
||||
const StyledRouteTriggerTableHeaderRowWrapper = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: flex;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px dashed ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
height: 160px;
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledCalloutWrapper = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTriggersTab = ({
|
||||
logicFunction,
|
||||
formValues,
|
||||
onChange,
|
||||
readonly = false,
|
||||
applicationName,
|
||||
}: {
|
||||
logicFunction: LogicFunction;
|
||||
formValues: LogicFunctionFormValues;
|
||||
onChange: <TKey extends keyof LogicFunctionFormValues>(
|
||||
key: TKey,
|
||||
) => (value: LogicFunctionFormValues[TKey]) => void;
|
||||
readonly?: boolean;
|
||||
applicationName?: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const cronTrigger = logicFunction.cronTriggerSettings;
|
||||
const hasAnyTrigger =
|
||||
isDefined(formValues.httpRouteTriggerSettings) ||
|
||||
isDefined(formValues.cronTriggerSettings) ||
|
||||
isDefined(formValues.databaseEventTriggerSettings) ||
|
||||
formValues.isTool;
|
||||
|
||||
const routeTrigger = logicFunction.httpRouteTriggerSettings;
|
||||
|
||||
const databaseEventTriggerSettings =
|
||||
logicFunction.databaseEventTriggerSettings;
|
||||
|
||||
let databaseEventTrigger = undefined;
|
||||
|
||||
if (isDefined(databaseEventTriggerSettings)) {
|
||||
const [object, action]: [string, string] =
|
||||
databaseEventTriggerSettings.eventName.split('.');
|
||||
databaseEventTrigger = {
|
||||
object,
|
||||
action,
|
||||
updatedFields: databaseEventTriggerSettings.updatedFields,
|
||||
};
|
||||
}
|
||||
const hasNoTriggers = !cronTrigger && !routeTrigger && !databaseEventTrigger;
|
||||
|
||||
if (hasNoTriggers) {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Triggers`}
|
||||
description={t`Configure when this function should be executed`}
|
||||
if (readonly && !hasAnyTrigger) {
|
||||
return isDefined(applicationName) ? (
|
||||
<StyledCalloutWrapper>
|
||||
<Callout
|
||||
variant="info"
|
||||
Icon={IconInfoCircle}
|
||||
title={t`Bundled with ${applicationName}`}
|
||||
description={t`This function has no trigger configured, so it can only be invoked from the Test tab or by other functions.`}
|
||||
/>
|
||||
<StyledEmptyState>
|
||||
{t`No triggers configured for this function.`}
|
||||
</StyledEmptyState>
|
||||
</Section>
|
||||
</StyledCalloutWrapper>
|
||||
) : (
|
||||
<StyledEmptyState>
|
||||
{t`No trigger is configured for this function.`}
|
||||
</StyledEmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDefined(databaseEventTrigger) && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Database event`}
|
||||
description={t`Select the events that should trigger the function`}
|
||||
/>
|
||||
<SettingsDatabaseEventsForm
|
||||
events={[databaseEventTrigger]}
|
||||
disabled
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{isDefined(cronTrigger) && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Cron`}
|
||||
description={t`Triggers the function at regular intervals`}
|
||||
/>
|
||||
<FormTextFieldInput
|
||||
label={t`Expression`}
|
||||
placeholder="0 */1 * * *"
|
||||
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
|
||||
onChange={() => {}}
|
||||
readonly
|
||||
defaultValue={cronTrigger.pattern}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{isDefined(routeTrigger) && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Http`}
|
||||
description={t`Triggers the function with Http request`}
|
||||
/>
|
||||
<Table>
|
||||
<StyledRouteTriggerTableHeaderRowWrapper>
|
||||
<TableRow
|
||||
gridTemplateColumns={ROUTE_TRIGGER_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Path`}</TableHeader>
|
||||
<TableHeader>{t`Method`}</TableHeader>
|
||||
<TableHeader>{t`Auth Required`}</TableHeader>
|
||||
</TableRow>
|
||||
</StyledRouteTriggerTableHeaderRowWrapper>
|
||||
<TableRow gridTemplateColumns={ROUTE_TRIGGER_GRID_TEMPLATE_COLUMNS}>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
<OverflowingTextWithTooltip
|
||||
text={`${REACT_APP_SERVER_BASE_URL}/s${routeTrigger.path}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
{routeTrigger.httpMethod}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Tag
|
||||
text={routeTrigger.isAuthRequired ? t`True` : t`False`}
|
||||
color={routeTrigger.isAuthRequired ? 'green' : 'orange'}
|
||||
weight="medium"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
</Section>
|
||||
<SettingsLogicFunctionHttpTriggerSection
|
||||
value={formValues.httpRouteTriggerSettings}
|
||||
onChange={onChange('httpRouteTriggerSettings')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionCronTriggerSection
|
||||
value={formValues.cronTriggerSettings}
|
||||
onChange={onChange('cronTriggerSettings')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionDatabaseEventTriggerSection
|
||||
value={formValues.databaseEventTriggerSettings}
|
||||
onChange={onChange('databaseEventTriggerSettings')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionToolTriggerSection
|
||||
isTool={formValues.isTool}
|
||||
toolInputSchema={formValues.toolInputSchema}
|
||||
onChange={onChange('isTool')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{!readonly && !hasAnyTrigger && (
|
||||
<StyledEmptyState>
|
||||
{t`No trigger is enabled. Toggle one of the options above to choose how this function gets invoked.`}
|
||||
</StyledEmptyState>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { SettingsLogicFunctionTriggerPayloadFormat } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerPayloadFormat';
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type CronTriggerSettings } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const DEFAULT_CRON_SETTINGS: CronTriggerSettings = {
|
||||
pattern: '0 */1 * * *',
|
||||
};
|
||||
|
||||
const StyledHint = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type SettingsLogicFunctionCronTriggerSectionProps = {
|
||||
value: CronTriggerSettings | null;
|
||||
onChange: (value: CronTriggerSettings | null) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionCronTriggerSection = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionCronTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`Cron`}
|
||||
description={t`Triggers the function at regular intervals`}
|
||||
enabled={isDefined(value)}
|
||||
onEnabledChange={(checked) =>
|
||||
onChange(checked ? DEFAULT_CRON_SETTINGS : null)
|
||||
}
|
||||
readonly={readonly}
|
||||
>
|
||||
{isDefined(value) && (
|
||||
<>
|
||||
<SettingsTextInput
|
||||
instanceId="logic-function-cron-trigger-pattern"
|
||||
label={t`Expression`}
|
||||
placeholder="0 */1 * * *"
|
||||
value={value.pattern}
|
||||
onChange={(newPattern: string) =>
|
||||
onChange({ ...value, pattern: newPattern })
|
||||
}
|
||||
readOnly={readonly}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledHint>
|
||||
{t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
|
||||
</StyledHint>
|
||||
<SettingsLogicFunctionTriggerPayloadFormat
|
||||
payload={{}}
|
||||
hint={t`Cron triggers pass no payload — the handler is called with an empty object.`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsLogicFunctionTriggerSection>
|
||||
);
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
|
||||
import { SettingsLogicFunctionTriggerPayloadFormat } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerPayloadFormat';
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { buildDatabaseEventPayload } from '@/settings/logic-functions/utils/getTriggerSamplePayload';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type DatabaseEventTriggerSettings } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DEFAULT_DATABASE_EVENT_SETTINGS: DatabaseEventTriggerSettings = {
|
||||
eventName: '*.created',
|
||||
};
|
||||
|
||||
type SettingsLogicFunctionDatabaseEventTriggerSectionProps = {
|
||||
value: DatabaseEventTriggerSettings | null;
|
||||
onChange: (value: DatabaseEventTriggerSettings | null) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionDatabaseEventTriggerSection = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionDatabaseEventTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [object = '', action = 'created'] = value?.eventName.split('.') ?? [];
|
||||
|
||||
const updateEventNamePart = ({
|
||||
field,
|
||||
fieldValue,
|
||||
}: {
|
||||
field: 'object' | 'action';
|
||||
fieldValue: string | null;
|
||||
}) => {
|
||||
if (!isDefined(value)) return;
|
||||
const nextObject = field === 'object' ? (fieldValue ?? '') : object;
|
||||
const nextAction = field === 'action' ? (fieldValue ?? action) : action;
|
||||
onChange({ ...value, eventName: `${nextObject}.${nextAction}` });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`Database event`}
|
||||
description={t`Triggers the function when a record changes`}
|
||||
enabled={isDefined(value)}
|
||||
onEnabledChange={(checked) =>
|
||||
onChange(checked ? DEFAULT_DATABASE_EVENT_SETTINGS : null)
|
||||
}
|
||||
readonly={readonly}
|
||||
>
|
||||
{isDefined(value) && (
|
||||
<>
|
||||
<SettingsDatabaseEventsForm
|
||||
events={[
|
||||
{
|
||||
object: object || null,
|
||||
action,
|
||||
updatedFields: value.updatedFields,
|
||||
},
|
||||
]}
|
||||
updateOperation={(_, field, fieldValue) =>
|
||||
updateEventNamePart({ field, fieldValue })
|
||||
}
|
||||
removeOperation={() => onChange(null)}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionTriggerPayloadFormat
|
||||
payload={buildDatabaseEventPayload(value)}
|
||||
hint={t`Your handler receives this event object. "after" holds the new state, "before" the previous one (null for created), and "updatedFields" lists the field names that changed on update.`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsLogicFunctionTriggerSection>
|
||||
);
|
||||
};
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { SettingsLogicFunctionTriggerPayloadFormat } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerPayloadFormat';
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { buildHttpPayload } from '@/settings/logic-functions/utils/getTriggerSamplePayload';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { type HttpRouteTriggerSettings } from 'twenty-shared/application';
|
||||
import { HTTPMethod } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconCopy,
|
||||
IconHttpDelete,
|
||||
IconHttpGet,
|
||||
IconHttpPatch,
|
||||
IconHttpPost,
|
||||
IconHttpPut,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { Toggle } from 'twenty-ui/input';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const HTTP_METHOD_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: HTTPMethod;
|
||||
Icon: IconComponent;
|
||||
}> = [
|
||||
{ label: 'GET', value: HTTPMethod.GET, Icon: IconHttpGet },
|
||||
{ label: 'POST', value: HTTPMethod.POST, Icon: IconHttpPost },
|
||||
{ label: 'PUT', value: HTTPMethod.PUT, Icon: IconHttpPut },
|
||||
{ label: 'PATCH', value: HTTPMethod.PATCH, Icon: IconHttpPatch },
|
||||
{ label: 'DELETE', value: HTTPMethod.DELETE, Icon: IconHttpDelete },
|
||||
];
|
||||
|
||||
const DEFAULT_HTTP_SETTINGS: HttpRouteTriggerSettings = {
|
||||
path: '',
|
||||
httpMethod: HTTPMethod.POST,
|
||||
isAuthRequired: false,
|
||||
};
|
||||
|
||||
const StyledFields = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledAuthRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledAuthLabel = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
`;
|
||||
|
||||
type SettingsLogicFunctionHttpTriggerSectionProps = {
|
||||
value: HttpRouteTriggerSettings | null;
|
||||
onChange: (value: HttpRouteTriggerSettings | null) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionHttpTriggerSection = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionHttpTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const updateField = <TKey extends keyof HttpRouteTriggerSettings>(
|
||||
key: TKey,
|
||||
fieldValue: HttpRouteTriggerSettings[TKey],
|
||||
) => {
|
||||
if (!isDefined(value)) {
|
||||
return;
|
||||
}
|
||||
onChange({ ...value, [key]: fieldValue });
|
||||
};
|
||||
|
||||
const fullUrl = isDefined(value)
|
||||
? `${REACT_APP_SERVER_BASE_URL}/s${value.path}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`HTTP`}
|
||||
description={t`Triggers the function with an HTTP request`}
|
||||
enabled={isDefined(value)}
|
||||
onEnabledChange={(checked) =>
|
||||
onChange(checked ? DEFAULT_HTTP_SETTINGS : null)
|
||||
}
|
||||
readonly={readonly}
|
||||
>
|
||||
{isDefined(value) && (
|
||||
<StyledFields>
|
||||
<Select
|
||||
dropdownId="logic-function-http-trigger-method"
|
||||
label={t`Method`}
|
||||
fullWidth
|
||||
disabled={readonly}
|
||||
value={value.httpMethod as HTTPMethod}
|
||||
options={HTTP_METHOD_OPTIONS}
|
||||
onChange={(newMethod) => updateField('httpMethod', newMethod)}
|
||||
dropdownOffset={{ y: 4 }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="logic-function-http-trigger-path"
|
||||
label={t`Path`}
|
||||
placeholder="/my-route"
|
||||
value={value.path}
|
||||
onChange={(newPath: string) => updateField('path', newPath)}
|
||||
readOnly={readonly}
|
||||
fullWidth
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="logic-function-http-trigger-url"
|
||||
label={t`Live URL`}
|
||||
value={fullUrl}
|
||||
onChange={() => {}}
|
||||
readOnly
|
||||
fullWidth
|
||||
RightIcon={IconCopy}
|
||||
onRightIconClick={() =>
|
||||
copyToClipboard(fullUrl, t`URL copied to clipboard`)
|
||||
}
|
||||
/>
|
||||
<StyledAuthRow>
|
||||
<Toggle
|
||||
value={value.isAuthRequired}
|
||||
onChange={(checked) => updateField('isAuthRequired', checked)}
|
||||
disabled={readonly}
|
||||
toggleSize="small"
|
||||
color={theme.color.blue}
|
||||
/>
|
||||
<StyledAuthLabel>{t`Require authentication`}</StyledAuthLabel>
|
||||
</StyledAuthRow>
|
||||
<SettingsLogicFunctionTriggerPayloadFormat
|
||||
payload={buildHttpPayload(value)}
|
||||
hint={
|
||||
value.httpMethod === HTTPMethod.GET
|
||||
? t`Your handler receives this object. The body is empty because GET requests carry no payload.`
|
||||
: t`Your handler receives this object. The body holds the parsed JSON sent by the client.`
|
||||
}
|
||||
/>
|
||||
</StyledFields>
|
||||
)}
|
||||
</SettingsLogicFunctionTriggerSection>
|
||||
);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsToolParameterTable } from '~/pages/settings/ai/components/SettingsToolParameterTable';
|
||||
|
||||
type ToolInputSchema = {
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
type SettingsLogicFunctionToolTriggerSectionProps = {
|
||||
isTool: boolean;
|
||||
toolInputSchema?: object;
|
||||
onChange: (value: boolean) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionToolTriggerSection = ({
|
||||
isTool,
|
||||
toolInputSchema,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionToolTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const schema = (toolInputSchema as ToolInputSchema | undefined) ?? {};
|
||||
const schemaProperties = isDefined(schema.properties)
|
||||
? (schema.properties as Record<
|
||||
string,
|
||||
{ type?: string; description?: string; format?: string }
|
||||
>)
|
||||
: {};
|
||||
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`AI tool`}
|
||||
description={t`Triggers the function when called by an AI agent or workflow`}
|
||||
enabled={isTool}
|
||||
onEnabledChange={onChange}
|
||||
readonly={readonly}
|
||||
>
|
||||
<SettingsToolParameterTable
|
||||
schemaProperties={schemaProperties}
|
||||
requiredFields={schema.required}
|
||||
/>
|
||||
</SettingsLogicFunctionTriggerSection>
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledHint = styled.span`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTriggerPayloadFormat = ({
|
||||
payload,
|
||||
hint,
|
||||
}: {
|
||||
payload: object;
|
||||
hint?: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabel>{t`Sample input`}</StyledLabel>
|
||||
<CodeEditor
|
||||
value={JSON.stringify(payload, null, 2)}
|
||||
language="json"
|
||||
height={140}
|
||||
options={{ readOnly: true }}
|
||||
/>
|
||||
{hint !== undefined && <StyledHint>{hint}</StyledHint>}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Toggle } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
type SettingsLogicFunctionTriggerSectionProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
readonly: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
// Common scaffolding for the four trigger toggles on a logic function. Hides
|
||||
// the section entirely when an installed app function doesn't use this trigger
|
||||
// (read-only + disabled = nothing to show).
|
||||
export const SettingsLogicFunctionTriggerSection = ({
|
||||
title,
|
||||
description,
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
readonly,
|
||||
children,
|
||||
}: SettingsLogicFunctionTriggerSectionProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
if (readonly && !enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledHeader>
|
||||
<H2Title title={title} description={description} />
|
||||
{!readonly && (
|
||||
<Toggle
|
||||
value={enabled}
|
||||
onChange={onEnabledChange}
|
||||
toggleSize="small"
|
||||
color={theme.color.blue}
|
||||
/>
|
||||
)}
|
||||
</StyledHeader>
|
||||
{enabled && children}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user