Skip to content

Commit

Permalink
Add available variables dropdown (#7964)
Browse files Browse the repository at this point in the history
- Add variable dropdown
- Insert variables on click
- Save variable as `{{stepName.object.myVar}}` and display only `myVar`


https://github.com/user-attachments/assets/9b49e32c-15e6-4b64-9901-0e63664bc3e8
  • Loading branch information
thomtrp authored Oct 23, 2024
1 parent 18778c5 commit 2e8b845
Show file tree
Hide file tree
Showing 17 changed files with 997 additions and 5 deletions.
6 changes: 6 additions & 0 deletions packages/twenty-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"@nivo/calendar": "^0.87.0",
"@nivo/core": "^0.87.0",
"@nivo/line": "^0.87.0",
"@tiptap/extension-document": "^2.9.0",
"@tiptap/extension-paragraph": "^2.9.0",
"@tiptap/extension-placeholder": "^2.9.0",
"@tiptap/extension-text": "^2.9.0",
"@tiptap/extension-text-style": "^2.8.0",
"@tiptap/react": "^2.8.0",
"@xyflow/react": "^12.0.4",
"transliteration": "^2.3.5"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export const PhoneCountryPickerDropdownButton = ({

const [selectedCountry, setSelectedCountry] = useState<Country>();

const { isDropdownOpen, closeDropdown } = useDropdown('country-picker');
const { isDropdownOpen, closeDropdown } = useDropdown(
CountryPickerHotkeyScope.CountryPicker,
);

const handleChange = (countryCode: string) => {
onChange(countryCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';
import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase';
import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { WorkflowSendEmailStep } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
Expand Down Expand Up @@ -208,7 +208,8 @@ export const WorkflowEditActionFormSendEmail = (
name="email"
control={form.control}
render={({ field }) => (
<TextInput
<VariableTagInput
inputId="email-input"
label="Email"
placeholder="Enter receiver email (use {{variable}} for dynamic content)"
value={field.value}
Expand All @@ -223,7 +224,8 @@ export const WorkflowEditActionFormSendEmail = (
name="subject"
control={form.control}
render={({ field }) => (
<TextInput
<VariableTagInput
inputId="email-subject-input"
label="Subject"
placeholder="Enter email subject (use {{variable}} for dynamic content)"
value={field.value}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem';
import SearchVariablesDropdownStepSubItem from '@/workflow/search-variables/components/SearchVariablesDropdownStepSubItem';
import { AVAILABLE_VARIABLES_MOCK } from '@/workflow/search-variables/constants/AvailableVariablesMock';
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/search-variables/constants/SearchVariablesDropdownId';
import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Editor } from '@tiptap/react';
import { useState } from 'react';
import { IconVariable } from 'twenty-ui';

const StyledDropdownVariableButtonContainer = styled(
StyledDropdownButtonContainer,
)`
background-color: ${({ theme }) => theme.background.transparent.lighter};
color: ${({ theme }) => theme.font.color.tertiary};
padding: ${({ theme }) => theme.spacing(0)};
margin: ${({ theme }) => theme.spacing(2)};
`;

const SearchVariablesDropdown = ({
inputId,
editor,
}: {
inputId: string;
editor: Editor;
}) => {
const theme = useTheme();

const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
const { isDropdownOpen } = useDropdown(dropdownId);
const [selectedStep, setSelectedStep] = useState<
WorkflowStepMock | undefined
>(undefined);

const insertVariableTag = (variable: string) => {
editor.commands.insertVariableTag(variable);
};

const handleStepSelect = (stepId: string) => {
setSelectedStep(
AVAILABLE_VARIABLES_MOCK.find((step) => step.id === stepId),
);
};

const handleSubItemSelect = (subItem: string) => {
insertVariableTag(subItem);
};

const handleBack = () => {
setSelectedStep(undefined);
};

return (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,
}}
clickableComponent={
<StyledDropdownVariableButtonContainer isUnfolded={isDropdownOpen}>
<IconVariable size={theme.icon.size.sm} />
</StyledDropdownVariableButtonContainer>
}
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
{selectedStep ? (
<SearchVariablesDropdownStepSubItem
step={selectedStep}
onSelect={handleSubItemSelect}
onBack={handleBack}
/>
) : (
<SearchVariablesDropdownStepItem
steps={AVAILABLE_VARIABLES_MOCK}
onSelect={handleStepSelect}
/>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
}
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 4 }}
/>
);
};

export default SearchVariablesDropdown;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock';

type SearchVariablesDropdownStepItemProps = {
steps: WorkflowStepMock[];
onSelect: (value: string) => void;
};

export const SearchVariablesDropdownStepItem = ({
steps,
onSelect,
}: SearchVariablesDropdownStepItemProps) => {
return (
<>
{steps.map((item, _index) => (
<MenuItemSelect
key={`step-${item.id}`}
selected={false}
hovered={false}
onClick={() => onSelect(item.id)}
text={item.name}
LeftIcon={undefined}
hasSubMenu
/>
))}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock';
import { isObject } from '@sniptt/guards';
import { useState } from 'react';
import { IconChevronLeft } from 'twenty-ui';

type SearchVariablesDropdownStepSubItemProps = {
step: WorkflowStepMock;
onSelect: (value: string) => void;
onBack: () => void;
};

const SearchVariablesDropdownStepSubItem = ({
step,
onSelect,
onBack,
}: SearchVariablesDropdownStepSubItemProps) => {
const [currentPath, setCurrentPath] = useState<string[]>([]);

const getSelectedObject = () => {
let selected = step.output;
for (const key of currentPath) {
selected = selected[key];
}
return selected;
};

const handleSelect = (key: string) => {
const selectedObject = getSelectedObject();
if (isObject(selectedObject[key])) {
setCurrentPath([...currentPath, key]);
} else {
onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`);
}
};

const goBack = () => {
if (currentPath.length === 0) {
onBack();
} else {
setCurrentPath(currentPath.slice(0, -1));
}
};

const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1);

return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
{headerLabel}
</DropdownMenuHeader>
{Object.entries(getSelectedObject()).map(([key, value]) => (
<MenuItemSelect
key={key}
selected={false}
hovered={false}
onClick={() => handleSelect(key)}
text={key}
hasSubMenu={isObject(value)}
LeftIcon={undefined}
/>
))}
</>
);
};

export default SearchVariablesDropdownStepSubItem;
Loading

0 comments on commit 2e8b845

Please sign in to comment.