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:
+2
@@ -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
|
||||
|
||||
+4
-18
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
-40
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+80
-50
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
},
|
||||
|
||||
+22
@@ -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,
|
||||
},
|
||||
];
|
||||
+3
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -116,6 +116,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
objectType: generatedMockObjectMetadataItems[0].nameSingular,
|
||||
outputSchema: {},
|
||||
icon: COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: false,
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
|
||||
+4
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user