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 { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
@ -39,13 +40,18 @@ export const WorkflowSingleRecordFieldChip = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: WorkflowSingleRecordFieldChipProps) => {
|
}: WorkflowSingleRecordFieldChipProps) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!!draftValue &&
|
!!draftValue &&
|
||||||
draftValue.type === 'variable' &&
|
draftValue.type === 'variable' &&
|
||||||
isStandaloneVariableString(draftValue.value)
|
isStandaloneVariableString(draftValue.value)
|
||||||
) {
|
) {
|
||||||
return (
|
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 { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||||
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
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 { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { isValidUuid } from '~/utils/isValidUuid';
|
import { isValidUuid } from '~/utils/isValidUuid';
|
||||||
|
|
||||||
const StyledFormSelectContainer = styled.div`
|
const StyledFormSelectContainer = styled.div`
|
||||||
@ -62,6 +61,16 @@ const StyledSearchVariablesDropdownContainer = styled.div`
|
|||||||
export type RecordId = string;
|
export type RecordId = string;
|
||||||
export type Variable = string;
|
export type Variable = string;
|
||||||
|
|
||||||
|
type WorkflowSingleRecordPickerValue =
|
||||||
|
| {
|
||||||
|
type: 'static';
|
||||||
|
value: RecordId;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'variable';
|
||||||
|
value: Variable;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowSingleRecordPickerProps = {
|
export type WorkflowSingleRecordPickerProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
defaultValue: RecordId | Variable;
|
defaultValue: RecordId | Variable;
|
||||||
@ -75,16 +84,7 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onChange,
|
onChange,
|
||||||
}: WorkflowSingleRecordPickerProps) => {
|
}: WorkflowSingleRecordPickerProps) => {
|
||||||
const [draftValue, setDraftValue] = useState<
|
const draftValue: WorkflowSingleRecordPickerValue =
|
||||||
| {
|
|
||||||
type: 'static';
|
|
||||||
value: RecordId;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'variable';
|
|
||||||
value: Variable;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
isStandaloneVariableString(defaultValue)
|
isStandaloneVariableString(defaultValue)
|
||||||
? {
|
? {
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
@ -93,10 +93,9 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
: {
|
: {
|
||||||
type: 'static',
|
type: 'static',
|
||||||
value: defaultValue || '',
|
value: defaultValue || '',
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const { record } = useFindOneRecord({
|
const { record: selectedRecord } = useFindOneRecord({
|
||||||
objectRecordId:
|
objectRecordId:
|
||||||
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
||||||
? defaultValue
|
? defaultValue
|
||||||
@ -106,10 +105,6 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
skip: !isValidUuid(defaultValue),
|
skip: !isValidUuid(defaultValue),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedRecord, setSelectedRecord] = useState<
|
|
||||||
ObjectRecord | undefined
|
|
||||||
>(record);
|
|
||||||
|
|
||||||
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
||||||
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
||||||
|
|
||||||
@ -126,32 +121,16 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
const handleRecordSelected = (
|
const handleRecordSelected = (
|
||||||
selectedEntity: RecordForSelect | null | undefined,
|
selectedEntity: RecordForSelect | null | undefined,
|
||||||
) => {
|
) => {
|
||||||
setDraftValue({
|
|
||||||
type: 'static',
|
|
||||||
value: selectedEntity?.record?.id ?? '',
|
|
||||||
});
|
|
||||||
setSelectedRecord(selectedEntity?.record);
|
|
||||||
closeDropdown();
|
|
||||||
|
|
||||||
onChange?.(selectedEntity?.record?.id ?? '');
|
onChange?.(selectedEntity?.record?.id ?? '');
|
||||||
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVariableTagInsert = (variable: string) => {
|
const handleVariableTagInsert = (variable: string) => {
|
||||||
setDraftValue({
|
|
||||||
type: 'variable',
|
|
||||||
value: variable,
|
|
||||||
});
|
|
||||||
setSelectedRecord(undefined);
|
|
||||||
closeDropdown();
|
|
||||||
|
|
||||||
onChange?.(variable);
|
onChange?.(variable);
|
||||||
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnlinkVariable = () => {
|
const handleUnlinkVariable = () => {
|
||||||
setDraftValue({
|
|
||||||
type: 'static',
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
|
||||||
onChange('');
|
onChange('');
|
||||||
@ -211,6 +190,7 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
inputId={variablesDropdownId}
|
inputId={variablesDropdownId}
|
||||||
onVariableSelect={handleVariableTagInsert}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
objectNameSingularToSelect={objectNameSingular}
|
||||||
/>
|
/>
|
||||||
</StyledSearchVariablesDropdownContainer>
|
</StyledSearchVariablesDropdownContainer>
|
||||||
</StyledFormFieldInputRowContainer>
|
</StyledFormFieldInputRowContainer>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
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 { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem';
|
import { SearchVariablesDropdownFieldItems } from '@/workflow/search-variables/components/SearchVariablesDropdownFieldItems';
|
||||||
import SearchVariablesDropdownStepSubItem from '@/workflow/search-variables/components/SearchVariablesDropdownStepSubItem';
|
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 { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/search-variables/constants/SearchVariablesDropdownId';
|
||||||
import { useAvailableVariablesInWorkflowStep } from '@/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep';
|
import { useAvailableVariablesInWorkflowStep } from '@/workflow/search-variables/hooks/useAvailableVariablesInWorkflowStep';
|
||||||
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IconVariablePlus } from 'twenty-ui';
|
import { IconVariablePlus, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledDropdownVariableButtonContainer = styled(
|
const StyledDropdownVariableButtonContainer = styled(
|
||||||
StyledDropdownButtonContainer,
|
StyledDropdownButtonContainer,
|
||||||
@ -26,21 +26,28 @@ const StyledDropdownVariableButtonContainer = styled(
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledDropdownComponetsContainer = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
`;
|
||||||
|
|
||||||
const SearchVariablesDropdown = ({
|
const SearchVariablesDropdown = ({
|
||||||
inputId,
|
inputId,
|
||||||
onVariableSelect,
|
onVariableSelect,
|
||||||
disabled,
|
disabled,
|
||||||
|
objectNameSingularToSelect,
|
||||||
}: {
|
}: {
|
||||||
inputId: string;
|
inputId: string;
|
||||||
onVariableSelect: (variableName: string) => void;
|
onVariableSelect: (variableName: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
objectNameSingularToSelect?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
|
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
|
||||||
const { isDropdownOpen } = useDropdown(dropdownId);
|
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
|
||||||
const availableVariablesInWorkflowStep =
|
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
|
||||||
useAvailableVariablesInWorkflowStep();
|
objectNameSingularToSelect,
|
||||||
|
});
|
||||||
|
|
||||||
const initialStep =
|
const initialStep =
|
||||||
availableVariablesInWorkflowStep.length === 1
|
availableVariablesInWorkflowStep.length === 1
|
||||||
@ -59,12 +66,44 @@ const SearchVariablesDropdown = ({
|
|||||||
|
|
||||||
const handleSubItemSelect = (subItem: string) => {
|
const handleSubItemSelect = (subItem: string) => {
|
||||||
onVariableSelect(subItem);
|
onVariableSelect(subItem);
|
||||||
|
setSelectedStep(undefined);
|
||||||
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setSelectedStep(undefined);
|
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) {
|
if (disabled === true) {
|
||||||
return (
|
return (
|
||||||
<StyledDropdownVariableButtonContainer
|
<StyledDropdownVariableButtonContainer
|
||||||
@ -97,20 +136,9 @@ const SearchVariablesDropdown = ({
|
|||||||
</StyledDropdownVariableButtonContainer>
|
</StyledDropdownVariableButtonContainer>
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<StyledDropdownComponetsContainer>
|
||||||
{selectedStep ? (
|
{renderSearchVariablesDropdownComponents()}
|
||||||
<SearchVariablesDropdownStepSubItem
|
</StyledDropdownComponetsContainer>
|
||||||
step={selectedStep}
|
|
||||||
onSelect={handleSubItemSelect}
|
|
||||||
onBack={handleBack}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SearchVariablesDropdownStepItem
|
|
||||||
steps={availableVariablesInWorkflowStep}
|
|
||||||
onSelect={handleStepSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
}
|
}
|
||||||
dropdownPlacement="bottom-end"
|
dropdownPlacement="bottom-end"
|
||||||
dropdownOffset={{ x: 0, y: 4 }}
|
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 { 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 { getTriggerStepName } from '@/workflow/search-variables/utils/getTriggerStepName';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
|
||||||
@ -9,7 +13,11 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||||
|
|
||||||
export const useAvailableVariablesInWorkflowStep = (): StepOutputSchema[] => {
|
export const useAvailableVariablesInWorkflowStep = ({
|
||||||
|
objectNameSingularToSelect,
|
||||||
|
}: {
|
||||||
|
objectNameSingularToSelect?: string;
|
||||||
|
}): StepOutputSchema[] => {
|
||||||
const workflowId = useRecoilValue(workflowIdState);
|
const workflowId = useRecoilValue(workflowIdState);
|
||||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||||
@ -42,29 +50,38 @@ export const useAvailableVariablesInWorkflowStep = (): StepOutputSchema[] => {
|
|||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
|
const filteredTriggerOutputSchema = filterOutputSchema(
|
||||||
|
workflow.currentVersion.trigger?.settings?.outputSchema as
|
||||||
|
| OutputSchema
|
||||||
|
| undefined,
|
||||||
|
objectNameSingularToSelect,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(workflow.currentVersion.trigger) &&
|
isDefined(workflow.currentVersion.trigger) &&
|
||||||
isDefined(workflow.currentVersion.trigger?.settings?.outputSchema) &&
|
isDefined(filteredTriggerOutputSchema) &&
|
||||||
!isEmptyObject(workflow.currentVersion.trigger?.settings?.outputSchema)
|
!isEmptyObject(filteredTriggerOutputSchema)
|
||||||
) {
|
) {
|
||||||
result.push({
|
result.push({
|
||||||
id: 'trigger',
|
id: 'trigger',
|
||||||
name: isDefined(workflow.currentVersion.trigger.name)
|
name: isDefined(workflow.currentVersion.trigger.name)
|
||||||
? workflow.currentVersion.trigger.name
|
? workflow.currentVersion.trigger.name
|
||||||
: getTriggerStepName(workflow.currentVersion.trigger),
|
: getTriggerStepName(workflow.currentVersion.trigger),
|
||||||
outputSchema: workflow.currentVersion.trigger.settings.outputSchema,
|
outputSchema: filteredTriggerOutputSchema,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
previousSteps.forEach((previousStep) => {
|
previousSteps.forEach((previousStep) => {
|
||||||
if (
|
const filteredOutputSchema = filterOutputSchema(
|
||||||
isDefined(previousStep.settings.outputSchema) &&
|
previousStep.settings.outputSchema as OutputSchema,
|
||||||
!isEmpty(previousStep.settings.outputSchema)
|
objectNameSingularToSelect,
|
||||||
) {
|
);
|
||||||
|
|
||||||
|
if (isDefined(filteredOutputSchema) && !isEmpty(filteredOutputSchema)) {
|
||||||
result.push({
|
result.push({
|
||||||
id: previousStep.id,
|
id: previousStep.id,
|
||||||
name: previousStep.name,
|
name: previousStep.name,
|
||||||
outputSchema: previousStep.settings.outputSchema,
|
outputSchema: filteredOutputSchema,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||||
|
|
||||||
type Leaf = {
|
export type Leaf = {
|
||||||
isLeaf: true;
|
isLeaf: true;
|
||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
label?: string;
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Node = {
|
export type Node = {
|
||||||
isLeaf: false;
|
isLeaf: false;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
label?: string;
|
||||||
value: OutputSchema;
|
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 = {
|
export type StepOutputSchema = {
|
||||||
id: string;
|
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 =
|
type FakeValueTypes =
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| Date
|
| Date
|
||||||
| FakeValueTypes[]
|
| FakeValueTypes[]
|
||||||
|
| FieldMetadataType
|
||||||
| { [key: string]: FakeValueTypes };
|
| { [key: string]: FakeValueTypes };
|
||||||
|
|
||||||
export const generateFakeValue = (valueType: string): FakeValueTypes => {
|
export const generateFakeValue = (valueType: string): FakeValueTypes => {
|
||||||
@ -35,6 +38,8 @@ export const generateFakeValue = (valueType: string): FakeValueTypes => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return objData;
|
return objData;
|
||||||
|
} else if (valueType === FieldMetadataType.TEXT) {
|
||||||
|
return 'My text';
|
||||||
} else {
|
} else {
|
||||||
return 'generated-string-value';
|
return 'generated-string-value';
|
||||||
}
|
}
|
||||||
|
@ -6,25 +6,25 @@ import { join } from 'path';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
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 { 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 { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||||
import {
|
import {
|
||||||
WorkflowVersionStepException,
|
WorkflowVersionStepException,
|
||||||
WorkflowVersionStepExceptionCode,
|
WorkflowVersionStepExceptionCode,
|
||||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
} 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 { 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';
|
const TRIGGER_STEP_ID = 'trigger';
|
||||||
|
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
import { InputSchemaPropertyType } from 'src/modules/code-introspection/types/input-schema.type';
|
||||||
|
|
||||||
type Leaf = {
|
export type Leaf = {
|
||||||
isLeaf: true;
|
isLeaf: true;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
type?: InputSchemaPropertyType;
|
type?: InputSchemaPropertyType;
|
||||||
|
label?: string;
|
||||||
value: any;
|
value: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Node = {
|
export type Node = {
|
||||||
isLeaf: false;
|
isLeaf: false;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
label?: string;
|
||||||
value: OutputSchema;
|
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 { 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 { 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 { 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 = (
|
export const generateFakeObjectRecordEvent = (
|
||||||
objectMetadataEntity: ObjectMetadataEntity,
|
objectMetadataEntity: ObjectMetadataEntity,
|
||||||
action: DatabaseEventAction,
|
action: DatabaseEventAction,
|
||||||
): OutputSchema => {
|
): BaseOutputSchema => {
|
||||||
const recordId = v4();
|
const recordId = v4();
|
||||||
const userId = v4();
|
const userId = v4();
|
||||||
const workspaceMemberId = v4();
|
const workspaceMemberId = v4();
|
||||||
@ -16,23 +16,30 @@ export const generateFakeObjectRecordEvent = (
|
|||||||
const after = generateFakeObjectRecord(objectMetadataEntity);
|
const after = generateFakeObjectRecord(objectMetadataEntity);
|
||||||
const formattedObjectMetadataEntity = Object.entries(
|
const formattedObjectMetadataEntity = Object.entries(
|
||||||
objectMetadataEntity,
|
objectMetadataEntity,
|
||||||
).reduce((acc: OutputSchema, [key, value]) => {
|
).reduce((acc: BaseOutputSchema, [key, value]) => {
|
||||||
acc[key] = { isLeaf: true, value };
|
acc[key] = { isLeaf: true, value };
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const baseResult: OutputSchema = {
|
const baseResult: BaseOutputSchema = {
|
||||||
recordId: { isLeaf: true, type: 'string', value: recordId },
|
recordId: {
|
||||||
userId: { isLeaf: true, type: 'string', value: userId },
|
isLeaf: true,
|
||||||
|
type: 'string',
|
||||||
|
value: recordId,
|
||||||
|
label: 'Record ID',
|
||||||
|
},
|
||||||
|
userId: { isLeaf: true, type: 'string', value: userId, label: 'User ID' },
|
||||||
workspaceMemberId: {
|
workspaceMemberId: {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: workspaceMemberId,
|
value: workspaceMemberId,
|
||||||
|
label: 'Workspace Member ID',
|
||||||
},
|
},
|
||||||
objectMetadata: {
|
objectMetadata: {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
value: formattedObjectMetadataEntity,
|
value: formattedObjectMetadataEntity,
|
||||||
|
label: 'Object Metadata',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +48,8 @@ export const generateFakeObjectRecordEvent = (
|
|||||||
...baseResult,
|
...baseResult,
|
||||||
properties: {
|
properties: {
|
||||||
isLeaf: false,
|
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: {
|
properties: {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
value: {
|
value: {
|
||||||
before: { isLeaf: false, value: before },
|
before: { isLeaf: false, value: before, label: 'Before' },
|
||||||
after: { isLeaf: false, value: after },
|
after: { isLeaf: false, value: after, label: 'After' },
|
||||||
},
|
},
|
||||||
|
label: 'Properties',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -67,8 +76,9 @@ export const generateFakeObjectRecordEvent = (
|
|||||||
properties: {
|
properties: {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
value: {
|
value: {
|
||||||
before: { isLeaf: false, value: before },
|
before: { isLeaf: false, value: before, label: 'Before' },
|
||||||
},
|
},
|
||||||
|
label: 'Properties',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -79,8 +89,9 @@ export const generateFakeObjectRecordEvent = (
|
|||||||
properties: {
|
properties: {
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
value: {
|
value: {
|
||||||
before: { isLeaf: false, value: before },
|
before: { isLeaf: false, value: before, label: 'Before' },
|
||||||
},
|
},
|
||||||
|
label: 'Properties',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,66 @@
|
|||||||
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
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 { 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 { camelToTitleCase } from 'src/utils/camel-to-title-case';
|
||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
||||||
|
const generateObjectRecordFields = (
|
||||||
|
objectMetadataEntity: ObjectMetadataEntity,
|
||||||
|
) =>
|
||||||
|
objectMetadataEntity.fields.reduce(
|
||||||
|
(acc: Record<string, Leaf | Node>, field) => {
|
||||||
|
if (!shouldGenerateFieldFakeValue(field)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||||
|
|
||||||
|
if (!compositeType) {
|
||||||
|
acc[field.name] = {
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
export const generateFakeObjectRecord = (
|
export const generateFakeObjectRecord = (
|
||||||
objectMetadataEntity: ObjectMetadataEntity,
|
objectMetadataEntity: ObjectMetadataEntity,
|
||||||
): OutputSchema =>
|
): RecordOutputSchema => ({
|
||||||
objectMetadataEntity.fields.reduce((acc: OutputSchema, field) => {
|
object: {
|
||||||
if (!shouldGenerateFieldFakeValue(field)) {
|
isLeaf: true,
|
||||||
return acc;
|
icon: objectMetadataEntity.icon,
|
||||||
}
|
label: objectMetadataEntity.labelSingular,
|
||||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
value: objectMetadataEntity.description,
|
||||||
|
nameSingular: objectMetadataEntity.nameSingular,
|
||||||
if (!compositeType) {
|
fieldIdName: 'id',
|
||||||
acc[field.name] = {
|
},
|
||||||
isLeaf: true,
|
fields: generateObjectRecordFields(objectMetadataEntity),
|
||||||
type: field.type,
|
_outputSchemaType: 'RECORD',
|
||||||
icon: field.icon,
|
});
|
||||||
value: generateFakeValue(field.type),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
acc[field.name] = {
|
|
||||||
isLeaf: false,
|
|
||||||
icon: field.icon,
|
|
||||||
value: compositeType.properties.reduce((acc, property) => {
|
|
||||||
acc[property.name] = {
|
|
||||||
isLeaf: true,
|
|
||||||
type: property.type,
|
|
||||||
value: generateFakeValue(property.type),
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
|
|
||||||
export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => {
|
export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => {
|
||||||
return (
|
return (
|
||||||
!field.isSystem &&
|
(!field.isSystem || field.name === 'id') &&
|
||||||
field.isActive &&
|
field.isActive &&
|
||||||
field.type !== FieldMetadataType.RELATION
|
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 { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
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 { 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 { 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';
|
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,
|
WorkflowTriggerType,
|
||||||
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
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()
|
@Injectable()
|
||||||
export class WorkflowBuilderWorkspaceService {
|
export class WorkflowBuilderWorkspaceService {
|
||||||
@ -145,7 +149,11 @@ export class WorkflowBuilderWorkspaceService {
|
|||||||
|
|
||||||
if (operationType === WorkflowRecordCRUDType.READ) {
|
if (operationType === WorkflowRecordCRUDType.READ) {
|
||||||
return {
|
return {
|
||||||
first: { isLeaf: false, icon: 'IconAlpha', value: recordOutputSchema },
|
first: {
|
||||||
|
isLeaf: false,
|
||||||
|
icon: 'IconAlpha',
|
||||||
|
value: recordOutputSchema,
|
||||||
|
},
|
||||||
last: { isLeaf: false, icon: 'IconOmega', value: recordOutputSchema },
|
last: { isLeaf: false, icon: 'IconOmega', value: recordOutputSchema },
|
||||||
totalCount: {
|
totalCount: {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
@ -231,7 +239,7 @@ export class WorkflowBuilderWorkspaceService {
|
|||||||
|
|
||||||
return resultFromFakeInput.data
|
return resultFromFakeInput.data
|
||||||
? Object.entries(resultFromFakeInput.data).reduce(
|
? Object.entries(resultFromFakeInput.data).reduce(
|
||||||
(acc: OutputSchema, [key, value]) => {
|
(acc: Record<string, Leaf | Node>, [key, value]) => {
|
||||||
acc[key] = {
|
acc[key] = {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
value,
|
value,
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { JSX } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { JSX } from 'react';
|
||||||
|
|
||||||
type HorizontalSeparatorProps = {
|
type HorizontalSeparatorProps = {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
noMargin?: boolean;
|
noMargin?: boolean;
|
||||||
|
color?: string;
|
||||||
};
|
};
|
||||||
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
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)};
|
height: ${({ visible }) => (visible ? '1px' : 0)};
|
||||||
margin-bottom: ${({ theme, noMargin }) => (noMargin ? 0 : theme.spacing(3))};
|
margin-bottom: ${({ theme, noMargin }) => (noMargin ? 0 : theme.spacing(3))};
|
||||||
margin-top: ${({ 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,
|
visible = true,
|
||||||
text = '',
|
text = '',
|
||||||
noMargin = false,
|
noMargin = false,
|
||||||
|
color,
|
||||||
}: HorizontalSeparatorProps): JSX.Element => (
|
}: HorizontalSeparatorProps): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{text ? (
|
{text ? (
|
||||||
@ -47,7 +49,7 @@ export const HorizontalSeparator = ({
|
|||||||
<StyledLine visible={visible} />
|
<StyledLine visible={visible} />
|
||||||
</StyledSeparatorContainer>
|
</StyledSeparatorContainer>
|
||||||
) : (
|
) : (
|
||||||
<StyledSeparator visible={visible} noMargin={noMargin} />
|
<StyledSeparator visible={visible} noMargin={noMargin} color={color} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user