mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
Select full record in variable dropdown (#8851)
Output schema is now separated in two sections: - object, that gather all informations on the selectable object - fields, that display object fields in a record context, or simply the available fields from the previous steps The dropdown variable has now a new mode: - if objectNameSingularToSelect is defined, it goes into an object mode. Only objects of the right type will be shown - if not set, it will use the already existing mode, to select a field When an object is selected, it actually set the id of the object https://github.com/user-attachments/assets/1c95f8fd-10f0-4c1c-aeb7-c7d847e89536
This commit is contained in:
parent
33e69805cb
commit
36e4357bb1
@ -1,3 +1,4 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -39,13 +40,18 @@ export const WorkflowSingleRecordFieldChip = ({
|
||||
objectNameSingular,
|
||||
onRemove,
|
||||
}: WorkflowSingleRecordFieldChipProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
if (
|
||||
!!draftValue &&
|
||||
draftValue.type === 'variable' &&
|
||||
isStandaloneVariableString(draftValue.value)
|
||||
) {
|
||||
return (
|
||||
<VariableChip rawVariableName={draftValue.value} onRemove={onRemove} />
|
||||
<VariableChip
|
||||
rawVariableName={objectMetadataItem.labelSingular}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import { SingleRecordSelect } from '@/object-record/relation-picker/components/S
|
||||
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
@ -22,7 +21,7 @@ import SearchVariablesDropdown from '@/workflow/search-variables/components/Sear
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { isValidUuid } from '~/utils/isValidUuid';
|
||||
|
||||
const StyledFormSelectContainer = styled.div`
|
||||
@ -62,6 +61,16 @@ const StyledSearchVariablesDropdownContainer = styled.div`
|
||||
export type RecordId = string;
|
||||
export type Variable = string;
|
||||
|
||||
type WorkflowSingleRecordPickerValue =
|
||||
| {
|
||||
type: 'static';
|
||||
value: RecordId;
|
||||
}
|
||||
| {
|
||||
type: 'variable';
|
||||
value: Variable;
|
||||
};
|
||||
|
||||
export type WorkflowSingleRecordPickerProps = {
|
||||
label?: string;
|
||||
defaultValue: RecordId | Variable;
|
||||
@ -75,16 +84,7 @@ export const WorkflowSingleRecordPicker = ({
|
||||
objectNameSingular,
|
||||
onChange,
|
||||
}: WorkflowSingleRecordPickerProps) => {
|
||||
const [draftValue, setDraftValue] = useState<
|
||||
| {
|
||||
type: 'static';
|
||||
value: RecordId;
|
||||
}
|
||||
| {
|
||||
type: 'variable';
|
||||
value: Variable;
|
||||
}
|
||||
>(
|
||||
const draftValue: WorkflowSingleRecordPickerValue =
|
||||
isStandaloneVariableString(defaultValue)
|
||||
? {
|
||||
type: 'variable',
|
||||
@ -93,10 +93,9 @@ export const WorkflowSingleRecordPicker = ({
|
||||
: {
|
||||
type: 'static',
|
||||
value: defaultValue || '',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const { record } = useFindOneRecord({
|
||||
const { record: selectedRecord } = useFindOneRecord({
|
||||
objectRecordId:
|
||||
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
||||
? defaultValue
|
||||
@ -106,10 +105,6 @@ export const WorkflowSingleRecordPicker = ({
|
||||
skip: !isValidUuid(defaultValue),
|
||||
});
|
||||
|
||||
const [selectedRecord, setSelectedRecord] = useState<
|
||||
ObjectRecord | undefined
|
||||
>(record);
|
||||
|
||||
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
||||
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
||||
|
||||
@ -126,32 +121,16 @@ export const WorkflowSingleRecordPicker = ({
|
||||
const handleRecordSelected = (
|
||||
selectedEntity: RecordForSelect | null | undefined,
|
||||
) => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: selectedEntity?.record?.id ?? '',
|
||||
});
|
||||
setSelectedRecord(selectedEntity?.record);
|
||||
closeDropdown();
|
||||
|
||||
onChange?.(selectedEntity?.record?.id ?? '');
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleVariableTagInsert = (variable: string) => {
|
||||
setDraftValue({
|
||||
type: 'variable',
|
||||
value: variable,
|
||||
});
|
||||
setSelectedRecord(undefined);
|
||||
closeDropdown();
|
||||
|
||||
onChange?.(variable);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: '',
|
||||
});
|
||||
closeDropdown();
|
||||
|
||||
onChange('');
|
||||
@ -211,6 +190,7 @@ export const WorkflowSingleRecordPicker = ({
|
||||
inputId={variablesDropdownId}
|
||||
onVariableSelect={handleVariableTagInsert}
|
||||
disabled={false}
|
||||
objectNameSingularToSelect={objectNameSingular}
|
||||
/>
|
||||
</StyledSearchVariablesDropdownContainer>
|
||||
</StyledFormFieldInputRowContainer>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
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 { SearchVariablesDropdownFieldItems } from '@/workflow/search-variables/components/SearchVariablesDropdownFieldItems';
|
||||
import { SearchVariablesDropdownObjectItems } from '@/workflow/search-variables/components/SearchVariablesDropdownObjectItems';
|
||||
import { SearchVariablesDropdownWorkflowStepItems } from '@/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems';
|
||||
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/search-variables/constants/SearchVariablesDropdownId';
|
||||
import { useAvailableVariablesInWorkflowStep } from '@/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep';
|
||||
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { IconVariablePlus } from 'twenty-ui';
|
||||
import { IconVariablePlus, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledDropdownVariableButtonContainer = styled(
|
||||
StyledDropdownButtonContainer,
|
||||
@ -26,21 +26,28 @@ const StyledDropdownVariableButtonContainer = styled(
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDropdownComponetsContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
`;
|
||||
|
||||
const SearchVariablesDropdown = ({
|
||||
inputId,
|
||||
onVariableSelect,
|
||||
disabled,
|
||||
objectNameSingularToSelect,
|
||||
}: {
|
||||
inputId: string;
|
||||
onVariableSelect: (variableName: string) => void;
|
||||
disabled?: boolean;
|
||||
objectNameSingularToSelect?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
|
||||
const { isDropdownOpen } = useDropdown(dropdownId);
|
||||
const availableVariablesInWorkflowStep =
|
||||
useAvailableVariablesInWorkflowStep();
|
||||
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
|
||||
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
|
||||
objectNameSingularToSelect,
|
||||
});
|
||||
|
||||
const initialStep =
|
||||
availableVariablesInWorkflowStep.length === 1
|
||||
@ -59,12 +66,44 @@ const SearchVariablesDropdown = ({
|
||||
|
||||
const handleSubItemSelect = (subItem: string) => {
|
||||
onVariableSelect(subItem);
|
||||
setSelectedStep(undefined);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedStep(undefined);
|
||||
};
|
||||
|
||||
const renderSearchVariablesDropdownComponents = () => {
|
||||
if (!isDefined(selectedStep)) {
|
||||
return (
|
||||
<SearchVariablesDropdownWorkflowStepItems
|
||||
dropdownId={dropdownId}
|
||||
steps={availableVariablesInWorkflowStep}
|
||||
onSelect={handleStepSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(objectNameSingularToSelect)) {
|
||||
return (
|
||||
<SearchVariablesDropdownObjectItems
|
||||
step={selectedStep}
|
||||
onSelect={handleSubItemSelect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchVariablesDropdownFieldItems
|
||||
step={selectedStep}
|
||||
onSelect={handleSubItemSelect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (disabled === true) {
|
||||
return (
|
||||
<StyledDropdownVariableButtonContainer
|
||||
@ -97,20 +136,9 @@ const SearchVariablesDropdown = ({
|
||||
</StyledDropdownVariableButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{selectedStep ? (
|
||||
<SearchVariablesDropdownStepSubItem
|
||||
step={selectedStep}
|
||||
onSelect={handleSubItemSelect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
) : (
|
||||
<SearchVariablesDropdownStepItem
|
||||
steps={availableVariablesInWorkflowStep}
|
||||
onSelect={handleStepSelect}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<StyledDropdownComponetsContainer>
|
||||
{renderSearchVariablesDropdownComponents()}
|
||||
</StyledDropdownComponetsContainer>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
|
@ -0,0 +1,133 @@
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import {
|
||||
BaseOutputSchema,
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
|
||||
import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconChevronLeft,
|
||||
MenuItemSelect,
|
||||
OverflowingTextWithTooltip,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
type SearchVariablesDropdownFieldItemsProps = {
|
||||
step: StepOutputSchema;
|
||||
onSelect: (value: string) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const SearchVariablesDropdownFieldItems = ({
|
||||
step,
|
||||
onSelect,
|
||||
onBack,
|
||||
}: SearchVariablesDropdownFieldItemsProps) => {
|
||||
const theme = useTheme();
|
||||
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const getCurrentSubStep = (): OutputSchema => {
|
||||
let currentSubStep = step.outputSchema;
|
||||
|
||||
for (const key of currentPath) {
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
currentSubStep = currentSubStep.fields[key]?.value;
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
currentSubStep = currentSubStep[key]?.value;
|
||||
}
|
||||
}
|
||||
|
||||
return currentSubStep;
|
||||
};
|
||||
|
||||
const getDisplayedSubStepFields = () => {
|
||||
const currentSubStep = getCurrentSubStep();
|
||||
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
return currentSubStep.fields;
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
return currentSubStep;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectField = (key: string) => {
|
||||
const currentSubStep = getCurrentSubStep();
|
||||
const handleSelectBaseOutputSchema = (
|
||||
baseOutputSchema: BaseOutputSchema,
|
||||
) => {
|
||||
if (!baseOutputSchema[key]?.isLeaf) {
|
||||
setCurrentPath([...currentPath, key]);
|
||||
setSearchInputValue('');
|
||||
} else {
|
||||
onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
handleSelectBaseOutputSchema(currentSubStep.fields);
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
handleSelectBaseOutputSchema(currentSubStep);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (currentPath.length === 0) {
|
||||
onBack();
|
||||
} else {
|
||||
setCurrentPath(currentPath.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1);
|
||||
const displayedObject = getDisplayedSubStepFields();
|
||||
const options = displayedObject ? Object.entries(displayedObject) : [];
|
||||
|
||||
const filteredOptions = searchInputValue
|
||||
? options.filter(
|
||||
([_, value]) =>
|
||||
value.label &&
|
||||
value.label.toLowerCase().includes(searchInputValue.toLowerCase()),
|
||||
)
|
||||
: options;
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
|
||||
<OverflowingTextWithTooltip text={headerLabel} />
|
||||
</DropdownMenuHeader>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
{filteredOptions.map(([key, value]) => (
|
||||
<MenuItemSelect
|
||||
key={key}
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={() => handleSelectField(key)}
|
||||
text={value.label || key}
|
||||
hasSubMenu={!value.isLeaf}
|
||||
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,159 @@
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import {
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
|
||||
import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconChevronLeft,
|
||||
MenuItemSelect,
|
||||
OverflowingTextWithTooltip,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
type SearchVariablesDropdownObjectItemsProps = {
|
||||
step: StepOutputSchema;
|
||||
onSelect: (value: string) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const SearchVariablesDropdownObjectItems = ({
|
||||
step,
|
||||
onSelect,
|
||||
onBack,
|
||||
}: SearchVariablesDropdownObjectItemsProps) => {
|
||||
const theme = useTheme();
|
||||
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const getCurrentSubStep = (): OutputSchema => {
|
||||
let currentSubStep = step.outputSchema;
|
||||
|
||||
for (const key of currentPath) {
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
currentSubStep = currentSubStep.fields[key]?.value;
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
currentSubStep = currentSubStep[key]?.value;
|
||||
}
|
||||
}
|
||||
|
||||
return currentSubStep;
|
||||
};
|
||||
|
||||
const getDisplayedSubStepFields = () => {
|
||||
const currentSubStep = getCurrentSubStep();
|
||||
|
||||
if (isRecordOutputSchema(currentSubStep)) {
|
||||
return currentSubStep.fields;
|
||||
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||
return currentSubStep;
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayedSubStepObject = () => {
|
||||
const currentSubStep = getCurrentSubStep();
|
||||
|
||||
return currentSubStep.object;
|
||||
};
|
||||
|
||||
const handleSelectObject = () => {
|
||||
const currentSubStep = getCurrentSubStep();
|
||||
|
||||
if (!isRecordOutputSchema(currentSubStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(
|
||||
`{{${step.id}.${[...currentPath, currentSubStep.object.fieldIdName].join('.')}}}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectField = (key: string) => {
|
||||
setCurrentPath([...currentPath, key]);
|
||||
setSearchInputValue('');
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (currentPath.length === 0) {
|
||||
onBack();
|
||||
} else {
|
||||
setCurrentPath(currentPath.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1);
|
||||
|
||||
const displayedSubStepObject = getDisplayedSubStepObject();
|
||||
|
||||
const shouldDisplaySubStepObject = searchInputValue
|
||||
? displayedSubStepObject?.label &&
|
||||
displayedSubStepObject.label
|
||||
.toLowerCase()
|
||||
.includes(searchInputValue.toLowerCase())
|
||||
: true;
|
||||
|
||||
const displayedFields = getDisplayedSubStepFields();
|
||||
const options = displayedFields ? Object.entries(displayedFields) : [];
|
||||
|
||||
const filteredOptions = searchInputValue
|
||||
? options.filter(
|
||||
([_, value]) =>
|
||||
value.label &&
|
||||
value.label.toLowerCase().includes(searchInputValue.toLowerCase()),
|
||||
)
|
||||
: options;
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
|
||||
<OverflowingTextWithTooltip text={headerLabel} />
|
||||
</DropdownMenuHeader>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
{shouldDisplaySubStepObject && displayedSubStepObject?.label && (
|
||||
<MenuItemSelect
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={handleSelectObject}
|
||||
text={displayedSubStepObject.label}
|
||||
hasSubMenu={false}
|
||||
LeftIcon={
|
||||
displayedSubStepObject.icon
|
||||
? getIcon(displayedSubStepObject.icon)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{filteredOptions.map(([key, value]) => (
|
||||
<MenuItemSelect
|
||||
key={key}
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={() => handleSelectField(key)}
|
||||
text={value.label || key}
|
||||
hasSubMenu={!value.isLeaf}
|
||||
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui';
|
||||
|
||||
type SearchVariablesDropdownStepItemProps = {
|
||||
steps: StepOutputSchema[];
|
||||
onSelect: (value: string) => void;
|
||||
};
|
||||
|
||||
export const SearchVariablesDropdownStepItem = ({
|
||||
steps,
|
||||
onSelect,
|
||||
}: SearchVariablesDropdownStepItemProps) => {
|
||||
return steps.length > 0 ? (
|
||||
<>
|
||||
{steps.map((item, _index) => (
|
||||
<MenuItemSelect
|
||||
key={`step-${item.id}`}
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={() => onSelect(item.id)}
|
||||
text={item.name}
|
||||
LeftIcon={undefined}
|
||||
hasSubMenu
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<MenuItem
|
||||
key="no-steps"
|
||||
onClick={() => {}}
|
||||
text="No variables available"
|
||||
LeftIcon={undefined}
|
||||
hasSubMenu={false}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,92 +0,0 @@
|
||||
import {
|
||||
OverflowingTextWithTooltip,
|
||||
IconChevronLeft,
|
||||
MenuItemSelect,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import {
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { useState } from 'react';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
|
||||
type SearchVariablesDropdownStepSubItemProps = {
|
||||
step: StepOutputSchema;
|
||||
onSelect: (value: string) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
const SearchVariablesDropdownStepSubItem = ({
|
||||
step,
|
||||
onSelect,
|
||||
onBack,
|
||||
}: SearchVariablesDropdownStepSubItemProps) => {
|
||||
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const getSelectedObject = (): OutputSchema => {
|
||||
let selected = step.outputSchema;
|
||||
for (const key of currentPath) {
|
||||
selected = selected[key]?.value;
|
||||
}
|
||||
return selected;
|
||||
};
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
const selectedObject = getSelectedObject();
|
||||
|
||||
if (!selectedObject[key]?.isLeaf) {
|
||||
setCurrentPath([...currentPath, key]);
|
||||
setSearchInputValue('');
|
||||
} 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);
|
||||
|
||||
const options = Object.entries(getSelectedObject());
|
||||
|
||||
const filteredOptions = searchInputValue
|
||||
? options.filter(([key]) =>
|
||||
key.toLowerCase().includes(searchInputValue.toLowerCase()),
|
||||
)
|
||||
: options;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={goBack}>
|
||||
<OverflowingTextWithTooltip text={headerLabel} />
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
{filteredOptions.map(([key, value]) => (
|
||||
<MenuItemSelect
|
||||
key={key}
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={() => handleSelect(key)}
|
||||
text={key}
|
||||
hasSubMenu={!value.isLeaf}
|
||||
LeftIcon={value.icon ? getIcon(value.icon) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchVariablesDropdownStepSubItem;
|
@ -0,0 +1,79 @@
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconX,
|
||||
MenuItem,
|
||||
MenuItemSelect,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
type SearchVariablesDropdownWorkflowStepItemsProps = {
|
||||
dropdownId: string;
|
||||
steps: StepOutputSchema[];
|
||||
onSelect: (value: string) => void;
|
||||
};
|
||||
|
||||
export const SearchVariablesDropdownWorkflowStepItems = ({
|
||||
dropdownId,
|
||||
steps,
|
||||
onSelect,
|
||||
}: SearchVariablesDropdownWorkflowStepItemsProps) => {
|
||||
const theme = useTheme();
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const availableSteps = steps.filter((step) =>
|
||||
searchInputValue
|
||||
? step.name.toLowerCase().includes(searchInputValue)
|
||||
: true,
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuHeader StartIcon={IconX} onClick={closeDropdown}>
|
||||
<OverflowingTextWithTooltip text={'Select Step'} />
|
||||
</DropdownMenuHeader>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
<HorizontalSeparator
|
||||
color={theme.background.transparent.primary}
|
||||
noMargin
|
||||
/>
|
||||
{availableSteps.length > 0 ? (
|
||||
availableSteps.map((item, _index) => (
|
||||
<MenuItemSelect
|
||||
key={`step-${item.id}`}
|
||||
selected={false}
|
||||
hovered={false}
|
||||
onClick={() => onSelect(item.id)}
|
||||
text={item.name}
|
||||
LeftIcon={undefined}
|
||||
hasSubMenu
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<MenuItem
|
||||
key="no-steps"
|
||||
onClick={() => {}}
|
||||
text="No variables available"
|
||||
LeftIcon={undefined}
|
||||
hasSubMenu={false}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
@ -1,5 +1,9 @@
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import {
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { filterOutputSchema } from '@/workflow/search-variables/utils/filterOutputSchema';
|
||||
import { getTriggerStepName } from '@/workflow/search-variables/utils/getTriggerStepName';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||
@ -9,7 +13,11 @@ import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
export const useAvailableVariablesInWorkflowStep = (): StepOutputSchema[] => {
|
||||
export const useAvailableVariablesInWorkflowStep = ({
|
||||
objectNameSingularToSelect,
|
||||
}: {
|
||||
objectNameSingularToSelect?: string;
|
||||
}): StepOutputSchema[] => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
@ -42,29 +50,38 @@ export const useAvailableVariablesInWorkflowStep = (): StepOutputSchema[] => {
|
||||
|
||||
const result = [];
|
||||
|
||||
const filteredTriggerOutputSchema = filterOutputSchema(
|
||||
workflow.currentVersion.trigger?.settings?.outputSchema as
|
||||
| OutputSchema
|
||||
| undefined,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(workflow.currentVersion.trigger) &&
|
||||
isDefined(workflow.currentVersion.trigger?.settings?.outputSchema) &&
|
||||
!isEmptyObject(workflow.currentVersion.trigger?.settings?.outputSchema)
|
||||
isDefined(filteredTriggerOutputSchema) &&
|
||||
!isEmptyObject(filteredTriggerOutputSchema)
|
||||
) {
|
||||
result.push({
|
||||
id: 'trigger',
|
||||
name: isDefined(workflow.currentVersion.trigger.name)
|
||||
? workflow.currentVersion.trigger.name
|
||||
: getTriggerStepName(workflow.currentVersion.trigger),
|
||||
outputSchema: workflow.currentVersion.trigger.settings.outputSchema,
|
||||
outputSchema: filteredTriggerOutputSchema,
|
||||
});
|
||||
}
|
||||
|
||||
previousSteps.forEach((previousStep) => {
|
||||
if (
|
||||
isDefined(previousStep.settings.outputSchema) &&
|
||||
!isEmpty(previousStep.settings.outputSchema)
|
||||
) {
|
||||
const filteredOutputSchema = filterOutputSchema(
|
||||
previousStep.settings.outputSchema as OutputSchema,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
|
||||
if (isDefined(filteredOutputSchema) && !isEmpty(filteredOutputSchema)) {
|
||||
result.push({
|
||||
id: previousStep.id,
|
||||
name: previousStep.name,
|
||||
outputSchema: previousStep.settings.outputSchema,
|
||||
outputSchema: filteredOutputSchema,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,19 +1,29 @@
|
||||
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||
|
||||
type Leaf = {
|
||||
export type Leaf = {
|
||||
isLeaf: true;
|
||||
type?: InputSchemaPropertyType;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
type Node = {
|
||||
export type Node = {
|
||||
isLeaf: false;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
value: OutputSchema;
|
||||
};
|
||||
|
||||
export type OutputSchema = Record<string, Leaf | Node>;
|
||||
export type BaseOutputSchema = Record<string, Leaf | Node>;
|
||||
|
||||
export type RecordOutputSchema = {
|
||||
object: { nameSingular: string; fieldIdName: string } & Leaf;
|
||||
fields: BaseOutputSchema;
|
||||
_outputSchemaType: 'RECORD';
|
||||
};
|
||||
|
||||
export type OutputSchema = BaseOutputSchema | RecordOutputSchema;
|
||||
|
||||
export type StepOutputSchema = {
|
||||
id: string;
|
||||
|
@ -0,0 +1,185 @@
|
||||
import { OutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { filterOutputSchema } from '../filterOutputSchema';
|
||||
|
||||
describe('filterOutputSchema', () => {
|
||||
describe('edge cases', () => {
|
||||
it('should return the input schema when objectNameSingularToSelect is undefined', () => {
|
||||
const inputSchema: OutputSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: {
|
||||
nameSingular: 'person',
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
},
|
||||
fields: {},
|
||||
};
|
||||
|
||||
expect(filterOutputSchema(inputSchema, undefined)).toBe(inputSchema);
|
||||
});
|
||||
|
||||
it('should return undefined when input schema is undefined', () => {
|
||||
expect(filterOutputSchema(undefined, 'person')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('record output schema', () => {
|
||||
const createRecordSchema = (
|
||||
nameSingular: string,
|
||||
fields = {},
|
||||
): OutputSchema => ({
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: {
|
||||
nameSingular,
|
||||
fieldIdName: 'id',
|
||||
isLeaf: true,
|
||||
value: 'Fake value',
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
it('should keep a matching record schema', () => {
|
||||
const inputSchema = createRecordSchema('person');
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(inputSchema);
|
||||
});
|
||||
|
||||
it('should filter out a non-matching record schema with no valid fields', () => {
|
||||
const inputSchema = createRecordSchema('company');
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep valid nested records while filtering out invalid ones', () => {
|
||||
const inputSchema = createRecordSchema('company', {
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person', {
|
||||
manager: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
}),
|
||||
},
|
||||
department: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('department'),
|
||||
},
|
||||
});
|
||||
|
||||
const expectedSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person', {
|
||||
manager: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema);
|
||||
});
|
||||
|
||||
it('should ignore leaf fields', () => {
|
||||
const inputSchema = createRecordSchema('company', {
|
||||
name: { isLeaf: true, value: 'string' },
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
});
|
||||
|
||||
const expectedSchema = {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: {
|
||||
employee: {
|
||||
isLeaf: false,
|
||||
value: createRecordSchema('person'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual(expectedSchema);
|
||||
});
|
||||
});
|
||||
|
||||
describe('base output schema', () => {
|
||||
const createBaseSchema = (fields = {}): OutputSchema => ({
|
||||
...fields,
|
||||
});
|
||||
|
||||
it('should filter out base schema with no valid records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: {
|
||||
isLeaf: true,
|
||||
value: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep base schema with valid nested records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual({
|
||||
field1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle deeply nested valid records', () => {
|
||||
const inputSchema = createBaseSchema({
|
||||
level1: {
|
||||
isLeaf: false,
|
||||
value: createBaseSchema({
|
||||
level2: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterOutputSchema(inputSchema, 'person')).toEqual({
|
||||
level1: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
level2: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
_outputSchemaType: 'RECORD',
|
||||
object: { nameSingular: 'person' },
|
||||
fields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,115 @@
|
||||
import {
|
||||
BaseOutputSchema,
|
||||
OutputSchema,
|
||||
RecordOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { isBaseOutputSchema } from '@/workflow/search-variables/utils/isBaseOutputSchema';
|
||||
import { isRecordOutputSchema } from '@/workflow/search-variables/utils/isRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const isValidRecordOutputSchema = (
|
||||
outputSchema: RecordOutputSchema,
|
||||
objectNameSingularToSelect?: string,
|
||||
): boolean => {
|
||||
if (isDefined(objectNameSingularToSelect)) {
|
||||
return (
|
||||
isDefined(outputSchema.object) &&
|
||||
outputSchema.object.nameSingular === objectNameSingularToSelect
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterRecordOutputSchema = (
|
||||
outputSchema: RecordOutputSchema,
|
||||
objectNameSingularToSelect: string,
|
||||
): RecordOutputSchema | undefined => {
|
||||
const filteredFields: BaseOutputSchema = {};
|
||||
let hasValidFields = false;
|
||||
|
||||
for (const key in outputSchema.fields) {
|
||||
const field = outputSchema.fields[key];
|
||||
|
||||
if (field.isLeaf) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validSubSchema = filterOutputSchema(
|
||||
field.value,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
if (isDefined(validSubSchema)) {
|
||||
filteredFields[key] = {
|
||||
...field,
|
||||
value: validSubSchema,
|
||||
};
|
||||
hasValidFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidRecordOutputSchema(outputSchema, objectNameSingularToSelect)) {
|
||||
return {
|
||||
...outputSchema,
|
||||
fields: filteredFields,
|
||||
};
|
||||
} else if (hasValidFields) {
|
||||
return {
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: filteredFields,
|
||||
} as RecordOutputSchema;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const filterBaseOutputSchema = (
|
||||
outputSchema: BaseOutputSchema,
|
||||
objectNameSingularToSelect: string,
|
||||
): BaseOutputSchema | undefined => {
|
||||
const filteredSchema: BaseOutputSchema = {};
|
||||
let hasValidFields = false;
|
||||
|
||||
for (const key in outputSchema) {
|
||||
const field = outputSchema[key];
|
||||
|
||||
if (field.isLeaf) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validSubSchema = filterOutputSchema(
|
||||
field.value,
|
||||
objectNameSingularToSelect,
|
||||
);
|
||||
if (isDefined(validSubSchema)) {
|
||||
filteredSchema[key] = {
|
||||
...field,
|
||||
value: validSubSchema,
|
||||
};
|
||||
hasValidFields = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidFields) {
|
||||
return filteredSchema;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const filterOutputSchema = (
|
||||
outputSchema?: OutputSchema,
|
||||
objectNameSingularToSelect?: string,
|
||||
): OutputSchema | undefined => {
|
||||
if (!objectNameSingularToSelect || !outputSchema) {
|
||||
return outputSchema;
|
||||
}
|
||||
|
||||
if (isRecordOutputSchema(outputSchema)) {
|
||||
return filterRecordOutputSchema(outputSchema, objectNameSingularToSelect);
|
||||
} else if (isBaseOutputSchema(outputSchema)) {
|
||||
return filterBaseOutputSchema(outputSchema, objectNameSingularToSelect);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
import {
|
||||
BaseOutputSchema,
|
||||
OutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
|
||||
export const isBaseOutputSchema = (
|
||||
outputSchema: OutputSchema,
|
||||
): outputSchema is BaseOutputSchema => {
|
||||
return !outputSchema._outputSchemaType;
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
import {
|
||||
OutputSchema,
|
||||
RecordOutputSchema,
|
||||
} from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
|
||||
export const isRecordOutputSchema = (
|
||||
outputSchema: OutputSchema,
|
||||
): outputSchema is RecordOutputSchema => {
|
||||
return outputSchema._outputSchemaType === 'RECORD';
|
||||
};
|
@ -1,9 +1,12 @@
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
type FakeValueTypes =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| FakeValueTypes[]
|
||||
| FieldMetadataType
|
||||
| { [key: string]: FakeValueTypes };
|
||||
|
||||
export const generateFakeValue = (valueType: string): FakeValueTypes => {
|
||||
@ -35,6 +38,8 @@ export const generateFakeValue = (valueType: string): FakeValueTypes => {
|
||||
});
|
||||
|
||||
return objData;
|
||||
} else if (valueType === FieldMetadataType.TEXT) {
|
||||
return 'My text';
|
||||
} else {
|
||||
return 'generated-string-value';
|
||||
}
|
||||
|
@ -6,25 +6,25 @@ import { join } from 'path';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||
import {
|
||||
WorkflowVersionStepException,
|
||||
WorkflowVersionStepExceptionCode,
|
||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||
import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
const TRIGGER_STEP_ID = 'trigger';
|
||||
|
||||
|
@ -1,16 +1,26 @@
|
||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
||||
|
||||
type Leaf = {
|
||||
export type Leaf = {
|
||||
isLeaf: true;
|
||||
icon?: string;
|
||||
type?: InputSchemaPropertyType;
|
||||
label?: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
type Node = {
|
||||
export type Node = {
|
||||
isLeaf: false;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
value: OutputSchema;
|
||||
};
|
||||
|
||||
export type OutputSchema = Record<string, Leaf | Node>;
|
||||
export type BaseOutputSchema = Record<string, Leaf | Node>;
|
||||
|
||||
export type RecordOutputSchema = {
|
||||
object: { nameSingular: string; fieldIdName: string } & Leaf;
|
||||
fields: BaseOutputSchema;
|
||||
_outputSchemaType: 'RECORD';
|
||||
};
|
||||
|
||||
export type OutputSchema = BaseOutputSchema | RecordOutputSchema;
|
||||
|
@ -2,13 +2,13 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { BaseOutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
|
||||
export const generateFakeObjectRecordEvent = (
|
||||
objectMetadataEntity: ObjectMetadataEntity,
|
||||
action: DatabaseEventAction,
|
||||
): OutputSchema => {
|
||||
): BaseOutputSchema => {
|
||||
const recordId = v4();
|
||||
const userId = v4();
|
||||
const workspaceMemberId = v4();
|
||||
@ -16,23 +16,30 @@ export const generateFakeObjectRecordEvent = (
|
||||
const after = generateFakeObjectRecord(objectMetadataEntity);
|
||||
const formattedObjectMetadataEntity = Object.entries(
|
||||
objectMetadataEntity,
|
||||
).reduce((acc: OutputSchema, [key, value]) => {
|
||||
).reduce((acc: BaseOutputSchema, [key, value]) => {
|
||||
acc[key] = { isLeaf: true, value };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const baseResult: OutputSchema = {
|
||||
recordId: { isLeaf: true, type: 'string', value: recordId },
|
||||
userId: { isLeaf: true, type: 'string', value: userId },
|
||||
const baseResult: BaseOutputSchema = {
|
||||
recordId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: recordId,
|
||||
label: 'Record ID',
|
||||
},
|
||||
userId: { isLeaf: true, type: 'string', value: userId, label: 'User ID' },
|
||||
workspaceMemberId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: workspaceMemberId,
|
||||
label: 'Workspace Member ID',
|
||||
},
|
||||
objectMetadata: {
|
||||
isLeaf: false,
|
||||
value: formattedObjectMetadataEntity,
|
||||
label: 'Object Metadata',
|
||||
},
|
||||
};
|
||||
|
||||
@ -41,7 +48,8 @@ export const generateFakeObjectRecordEvent = (
|
||||
...baseResult,
|
||||
properties: {
|
||||
isLeaf: false,
|
||||
value: { after: { isLeaf: false, value: after } },
|
||||
value: { after: { isLeaf: false, value: after, label: 'After' } },
|
||||
label: 'Properties',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -54,9 +62,10 @@ export const generateFakeObjectRecordEvent = (
|
||||
properties: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
before: { isLeaf: false, value: before },
|
||||
after: { isLeaf: false, value: after },
|
||||
before: { isLeaf: false, value: before, label: 'Before' },
|
||||
after: { isLeaf: false, value: after, label: 'After' },
|
||||
},
|
||||
label: 'Properties',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -67,8 +76,9 @@ export const generateFakeObjectRecordEvent = (
|
||||
properties: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
before: { isLeaf: false, value: before },
|
||||
before: { isLeaf: false, value: before, label: 'Before' },
|
||||
},
|
||||
label: 'Properties',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -79,8 +89,9 @@ export const generateFakeObjectRecordEvent = (
|
||||
properties: {
|
||||
isLeaf: false,
|
||||
value: {
|
||||
before: { isLeaf: false, value: before },
|
||||
before: { isLeaf: false, value: before, label: 'Before' },
|
||||
},
|
||||
label: 'Properties',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||
import {
|
||||
Leaf,
|
||||
Node,
|
||||
RecordOutputSchema,
|
||||
} from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { camelToTitleCase } from 'src/utils/camel-to-title-case';
|
||||
|
||||
export const generateFakeObjectRecord = (
|
||||
const generateObjectRecordFields = (
|
||||
objectMetadataEntity: ObjectMetadataEntity,
|
||||
): OutputSchema =>
|
||||
objectMetadataEntity.fields.reduce((acc: OutputSchema, field) => {
|
||||
) =>
|
||||
objectMetadataEntity.fields.reduce(
|
||||
(acc: Record<string, Leaf | Node>, field) => {
|
||||
if (!shouldGenerateFieldFakeValue(field)) {
|
||||
return acc;
|
||||
}
|
||||
@ -18,16 +24,19 @@ export const generateFakeObjectRecord = (
|
||||
isLeaf: true,
|
||||
type: field.type,
|
||||
icon: field.icon,
|
||||
label: field.label,
|
||||
value: generateFakeValue(field.type),
|
||||
};
|
||||
} else {
|
||||
acc[field.name] = {
|
||||
isLeaf: false,
|
||||
icon: field.icon,
|
||||
label: field.label,
|
||||
value: compositeType.properties.reduce((acc, property) => {
|
||||
acc[property.name] = {
|
||||
isLeaf: true,
|
||||
type: property.type,
|
||||
label: camelToTitleCase(property.name),
|
||||
value: generateFakeValue(property.type),
|
||||
};
|
||||
|
||||
@ -37,4 +46,21 @@ export const generateFakeObjectRecord = (
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
export const generateFakeObjectRecord = (
|
||||
objectMetadataEntity: ObjectMetadataEntity,
|
||||
): RecordOutputSchema => ({
|
||||
object: {
|
||||
isLeaf: true,
|
||||
icon: objectMetadataEntity.icon,
|
||||
label: objectMetadataEntity.labelSingular,
|
||||
value: objectMetadataEntity.description,
|
||||
nameSingular: objectMetadataEntity.nameSingular,
|
||||
fieldIdName: 'id',
|
||||
},
|
||||
fields: generateObjectRecordFields(objectMetadataEntity),
|
||||
_outputSchemaType: 'RECORD',
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
|
||||
export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => {
|
||||
return (
|
||||
!field.isSystem &&
|
||||
(!field.isSystem || field.name === 'id') &&
|
||||
field.isActive &&
|
||||
field.type !== FieldMetadataType.RELATION
|
||||
);
|
||||
|
@ -12,6 +12,12 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
||||
import {
|
||||
Leaf,
|
||||
Node,
|
||||
OutputSchema,
|
||||
} from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
|
||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event';
|
||||
import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
@ -24,8 +30,6 @@ import {
|
||||
WorkflowTriggerType,
|
||||
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/types/output-schema.type';
|
||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowBuilderWorkspaceService {
|
||||
@ -145,7 +149,11 @@ export class WorkflowBuilderWorkspaceService {
|
||||
|
||||
if (operationType === WorkflowRecordCRUDType.READ) {
|
||||
return {
|
||||
first: { isLeaf: false, icon: 'IconAlpha', value: recordOutputSchema },
|
||||
first: {
|
||||
isLeaf: false,
|
||||
icon: 'IconAlpha',
|
||||
value: recordOutputSchema,
|
||||
},
|
||||
last: { isLeaf: false, icon: 'IconOmega', value: recordOutputSchema },
|
||||
totalCount: {
|
||||
isLeaf: true,
|
||||
@ -231,7 +239,7 @@ export class WorkflowBuilderWorkspaceService {
|
||||
|
||||
return resultFromFakeInput.data
|
||||
? Object.entries(resultFromFakeInput.data).reduce(
|
||||
(acc: OutputSchema, [key, value]) => {
|
||||
(acc: Record<string, Leaf | Node>, [key, value]) => {
|
||||
acc[key] = {
|
||||
isLeaf: true,
|
||||
value,
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { JSX } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { JSX } from 'react';
|
||||
|
||||
type HorizontalSeparatorProps = {
|
||||
visible?: boolean;
|
||||
text?: string;
|
||||
noMargin?: boolean;
|
||||
color?: string;
|
||||
};
|
||||
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
||||
background-color: ${({ theme }) => theme.border.color.medium};
|
||||
background-color: ${({ theme, color }) => color ?? theme.border.color.medium};
|
||||
height: ${({ visible }) => (visible ? '1px' : 0)};
|
||||
margin-bottom: ${({ theme, noMargin }) => (noMargin ? 0 : theme.spacing(3))};
|
||||
margin-top: ${({ theme, noMargin }) => (noMargin ? 0 : theme.spacing(3))};
|
||||
@ -38,6 +39,7 @@ export const HorizontalSeparator = ({
|
||||
visible = true,
|
||||
text = '',
|
||||
noMargin = false,
|
||||
color,
|
||||
}: HorizontalSeparatorProps): JSX.Element => (
|
||||
<>
|
||||
{text ? (
|
||||
@ -47,7 +49,7 @@ export const HorizontalSeparator = ({
|
||||
<StyledLine visible={visible} />
|
||||
</StyledSeparatorContainer>
|
||||
) : (
|
||||
<StyledSeparator visible={visible} noMargin={noMargin} />
|
||||
<StyledSeparator visible={visible} noMargin={noMargin} color={color} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user