feat: ability to pin a manual trigger in the navbar (#13904)

Fixes #13333 

This PR adds the ability to pin a manual trigger in the navbar. As
suggested, the following changes have been made:

**Changes Made**

- **Schema updates**
- Updated the `workflowSchema.ts` and added `isPinned` to the
`workflowManualTriggerSchema` as an optional boolean.

- **Defaults**
- Updated the `getManualTriggerDefaultSettings.ts` to add `isPinned` to
the `WorkflowManualTriggerSettings`, defaulting to false.

- **UI updates**
- Updated the `ManualTriggerAvailabilityOptions.ts` to improve the
Availability labels as suggested in the figma files.
- Created a new file `ManualTriggerIsPinnedOptions.ts` that contains the
options (label, value and icon) for the `isPinned` select component.
- Updated the `WorkflowEditTriggerManualForm.tsx` to incorporate the
added `isPinned` select component.

- **Action handling**
- Modified `useRunWorkflowRecordActions.tsx` to add `isPinned:
activeWorkflowVersion.trigger?.settings?.isPinned` to the returned
action config.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
ahmedobaid23
2025-08-14 14:03:35 +05:00
committed by GitHub
parent 36fcb29e86
commit 455ce191e4
12 changed files with 123 additions and 110 deletions
@@ -82,8 +82,10 @@ export const useRunWorkflowRecordActions = ({
key: `workflow-run-${activeWorkflowVersion.id}`,
scope: ActionScope.RecordSelection,
label: msg`${name}`,
shortLabel: msg`${name}`,
position: index,
Icon,
isPinned: activeWorkflowVersion.trigger?.settings?.isPinned,
shouldBeRegistered: () => true,
component: (
<Action
@@ -1,7 +1,6 @@
import { type ActionMenuContextType } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuContextProviderWorkflowObjects } from '@/action-menu/contexts/ActionMenuContextProviderWorkflowObjects';
import { ActionMenuContextProviderWorkflowsEnabled } from '@/action-menu/contexts/ActionMenuContextProviderWorkflowsEnabled';
import { ActionMenuContextProviderWorkflowsNotEnabled } from '@/action-menu/contexts/ActionMenuContextProviderWorkflowsNotEnabled';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -46,31 +45,18 @@ export const ActionMenuContextProvider = ({
);
}
if (
isDefined(objectMetadataItem) &&
(actionMenuType === 'command-menu' ||
actionMenuType === 'command-menu-show-page-action-menu-dropdown')
) {
return (
<ActionMenuContextProviderWorkflowsEnabled
isInRightDrawer={isInRightDrawer}
displayType={displayType}
actionMenuType={actionMenuType}
objectMetadataItem={objectMetadataItem}
>
{children}
</ActionMenuContextProviderWorkflowsEnabled>
);
if (!isDefined(objectMetadataItem)) {
return null;
}
return (
<ActionMenuContextProviderWorkflowsNotEnabled
<ActionMenuContextProviderWorkflowsEnabled
isInRightDrawer={isInRightDrawer}
displayType={displayType}
actionMenuType={actionMenuType}
objectMetadataItem={objectMetadataItem}
>
{children}
</ActionMenuContextProviderWorkflowsNotEnabled>
</ActionMenuContextProviderWorkflowsEnabled>
);
};
@@ -1,40 +0,0 @@
import {
ActionMenuContext,
type ActionMenuContextType,
} from '@/action-menu/contexts/ActionMenuContext';
import { useRegisteredActions } from '@/action-menu/hooks/useRegisteredActions';
import { useShouldActionBeRegisteredParams } from '@/action-menu/hooks/useShouldActionBeRegisteredParams';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const ActionMenuContextProviderWorkflowsNotEnabled = ({
objectMetadataItem,
isInRightDrawer,
displayType,
actionMenuType,
children,
}: {
objectMetadataItem?: ObjectMetadataItem;
isInRightDrawer: ActionMenuContextType['isInRightDrawer'];
displayType: ActionMenuContextType['displayType'];
actionMenuType: ActionMenuContextType['actionMenuType'];
children: React.ReactNode;
}) => {
const params = useShouldActionBeRegisteredParams({
objectMetadataItem,
});
const actions = useRegisteredActions(params);
return (
<ActionMenuContext.Provider
value={{
isInRightDrawer,
displayType,
actionMenuType,
actions,
}}
>
{children}
</ActionMenuContext.Provider>
);
};
@@ -240,6 +240,7 @@ export const workflowManualTriggerSchema = baseTriggerSchema.extend({
objectType: z.string().optional(),
outputSchema: z.object({}).passthrough(),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
});
@@ -1,5 +1,8 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import {
type WorkflowManualTrigger,
type WorkflowManualTriggerAvailability,
@@ -7,19 +10,17 @@ import {
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useLingui } from '@lingui/react/macro';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { SelectControl } from '@/ui/input/components/SelectControl';
import styled from '@emotion/styled';
type WorkflowEditTriggerManualFormProps = {
trigger: WorkflowManualTrigger;
@@ -84,7 +85,7 @@ export const WorkflowEditTriggerManualForm = ({
const headerType = getTriggerHeaderType(trigger);
const availabilityDescriptions = {
WHEN_RECORD_SELECTED: t`Select a record then open the ⌘K to trigger this workflow`,
WHEN_RECORD_SELECTED: t`The selected record(s) will be passed to your workflow`,
EVERYWHERE: t`Open the ⌘K to trigger this workflow`,
};
@@ -108,9 +109,52 @@ export const WorkflowEditTriggerManualForm = ({
disabled={triggerOptions.readonly}
/>
<WorkflowStepBody>
<IconPicker
dropdownId={'workflow-edit-manual-trigger-icon'}
selectedIconKey={trigger.settings.icon}
dropdownOffset={{ y: -parseInt(theme.spacing(3), 10) }}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
maxIconsVisible={9 * 8} // 9 columns * 8 lines
disabled={triggerOptions.readonly}
clickableComponent={
<StyledIconPickerContainer
onClick={(e) => {
if (triggerOptions.readonly === true) {
e.stopPropagation();
e.preventDefault();
}
}}
>
<StyledLabel>{t`Command Icon`}</StyledLabel>
<SelectControl
isDisabled={triggerOptions.readonly}
selectedOption={{
Icon: getIcon(trigger.settings.icon),
value: trigger.settings.icon || null,
label: '',
}}
/>
<StyledDescription>{t`The icon your workflow trigger will display in the command menu`}</StyledDescription>
</StyledIconPickerContainer>
}
onChange={({ iconKey }) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
...trigger.settings,
icon: iconKey,
},
});
}}
/>
<Select
dropdownId={'workflow-edit-manual-trigger-availability'}
label={t`Available`}
label={t`Availability`}
description={availabilityDescriptions[manualTriggerAvailability]}
fullWidth
disabled={triggerOptions.readonly}
@@ -138,7 +182,7 @@ export const WorkflowEditTriggerManualForm = ({
<Select
dropdownId={'workflow-edit-manual-trigger-object'}
label={t`Object`}
description={t`Will return one ${objectType} to the next step of this workflow`}
description={t`On which object(s) should this trigger be available`}
fullWidth
value={objectType}
options={availableMetadata}
@@ -161,48 +205,34 @@ export const WorkflowEditTriggerManualForm = ({
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
) : null}
<IconPicker
dropdownId={'workflow-edit-manual-trigger-icon'}
selectedIconKey={trigger.settings.icon}
dropdownOffset={{ y: -parseInt(theme.spacing(3), 10) }}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
maxIconsVisible={9 * 8} // 9 columns * 8 lines
disabled={triggerOptions.readonly}
clickableComponent={
<StyledIconPickerContainer
onClick={(e) => {
if (triggerOptions.readonly === true) {
e.stopPropagation();
e.preventDefault();
}
}}
>
<StyledLabel>{t`Command menu icon`}</StyledLabel>
<SelectControl
isDisabled={triggerOptions.readonly}
selectedOption={{
Icon: getIcon(trigger.settings.icon),
value: trigger.settings.icon || null,
label: '',
}}
/>
<StyledDescription>{t`The icon your workflow trigger will display in the command menu`}</StyledDescription>
</StyledIconPickerContainer>
}
onChange={({ iconKey }) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
...trigger.settings,
icon: iconKey,
},
});
}}
/>
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
<Select
dropdownId={'workflow-edit-manual-trigger-navbar'}
label={t`Navbar`}
description={t`Display a button in the top navbar to trigger this workflow`}
fullWidth
value={trigger.settings.isPinned}
options={MANUAL_TRIGGER_IS_PINNED_OPTIONS}
disabled={triggerOptions.readonly}
onChange={(updatedValue) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
...trigger.settings,
isPinned: updatedValue,
outputSchema: {},
},
});
}}
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
) : null}
</WorkflowStepBody>
</>
);
@@ -11,12 +11,12 @@ export const MANUAL_TRIGGER_AVAILABILITY_OPTIONS: Array<{
Icon: IconComponent;
}> = [
{
label: 'When record is selected',
label: 'When records are selected',
value: 'WHEN_RECORD_SELECTED',
Icon: IconCheckbox,
},
{
label: 'When no record is selected',
label: 'When no records are selected',
value: 'EVERYWHERE',
Icon: IconSquare,
},
@@ -0,0 +1,22 @@
import {
type IconComponent,
IconPinned,
IconPinnedOff,
} from 'twenty-ui/display';
export const MANUAL_TRIGGER_IS_PINNED_OPTIONS: Array<{
label: string;
value: boolean;
Icon: IconComponent;
}> = [
{
label: 'Not Pinned',
value: false,
Icon: IconPinnedOff,
},
{
label: 'Pinned',
value: true,
Icon: IconPinned,
},
];
@@ -13,6 +13,7 @@ it('returns settings for a manual trigger that can be activated from any where',
objectType: undefined,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
});
});
@@ -27,6 +28,7 @@ it('returns settings for a manual trigger that can be activated from any where',
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: 'IconTest',
isPinned: false,
});
});
@@ -40,6 +42,7 @@ it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom i
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
});
});
@@ -116,6 +116,7 @@ describe('getTriggerDefaultDefinition', () => {
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
},
nextStepIds: [],
position: {
@@ -10,10 +10,12 @@ export const getManualTriggerDefaultSettings = ({
availability,
activeNonSystemObjectMetadataItems,
icon,
isPinned,
}: {
availability: WorkflowManualTriggerAvailability;
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
icon?: string;
isPinned?: boolean;
}): WorkflowManualTriggerSettings => {
switch (availability) {
case 'EVERYWHERE': {
@@ -21,6 +23,7 @@ export const getManualTriggerDefaultSettings = ({
objectType: undefined,
outputSchema: {},
icon: icon || COMMAND_MENU_DEFAULT_ICON,
isPinned: isPinned || false,
};
}
case 'WHEN_RECORD_SELECTED': {
@@ -28,6 +31,7 @@ export const getManualTriggerDefaultSettings = ({
objectType: activeNonSystemObjectMetadataItems[0].nameSingular,
outputSchema: {},
icon: icon || COMMAND_MENU_DEFAULT_ICON,
isPinned: isPinned || false,
};
}
}
@@ -327,6 +327,8 @@ export {
IconWebhook,
IconWorld,
IconX,
IconPinned,
IconPinnedOff,
} from '@tabler/icons-react';
export type { IconProps as TablerIconsProps } from '@tabler/icons-react';
+2
View File
@@ -389,6 +389,8 @@ export {
IconWebhook,
IconWorld,
IconX,
IconPinned,
IconPinnedOff,
} from './icon/components/TablerIcons';
export { useIcons } from './icon/hooks/useIcons';
export { IconsProvider } from './icon/providers/IconsProvider';