Only display Flow for Workflow Runs and display Output tab for triggers (#11520)
> [!WARNING] > I refactored a bunch of components into utility functions to make it possible to display the `WorkflowStepHeader` component for **triggers** in the `CommandMenuWorkflowRunViewStep` component. Previously, we were asserting that we were displaying the header in `Output` and `Input` tabs only for **actions**. Handling triggers too required a bunch of changes. We can think of making a bigger refactor of this part. In this PR: - Only display the Flow for Workflow Runs; removed the Code Editor tab - Allows users to see the Output of trigger nodes - Prevent impossible states by manually setting the selected tab when selecting a node ## Demo ### Success, Running and Not Executed steps https://github.com/user-attachments/assets/c6bebd0f-5da2-4ccc-aef2-d9890eafa59a ### Failed step https://github.com/user-attachments/assets/e1f4e13a-2f5e-4792-a089-928e4d6b1ac0 Closes https://github.com/twentyhq/core-team-issues/issues/709
This commit is contained in:
committed by
GitHub
parent
c8011da4d7
commit
e8488e1da0
@ -20,7 +20,6 @@ import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getSh
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
||||
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
@ -66,7 +65,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
}),
|
||||
WorkflowRunTabId.NODE,
|
||||
null,
|
||||
);
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -15,7 +17,6 @@ import {
|
||||
WorkflowRunTabIdType,
|
||||
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
||||
@ -45,38 +46,39 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const stepExecutionStatus = isDefined(workflowRun)
|
||||
? getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: workflowRun.output,
|
||||
stepId: workflowSelectedNode,
|
||||
})
|
||||
: undefined;
|
||||
if (!isDefined(workflowRun)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const areInputAndOutputTabsDisabled =
|
||||
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||
stepExecutionStatus === 'running' ||
|
||||
stepExecutionStatus === 'not-executed';
|
||||
const stepExecutionStatus = getWorkflowRunStepExecutionStatus({
|
||||
workflowRunOutput: workflowRun.output,
|
||||
stepId: workflowSelectedNode,
|
||||
});
|
||||
|
||||
const isInputTabDisabled = getIsInputTabDisabled({
|
||||
stepExecutionStatus,
|
||||
workflowSelectedNode,
|
||||
});
|
||||
const isOutputTabDisabled = getIsOutputTabDisabled({
|
||||
stepExecutionStatus,
|
||||
});
|
||||
|
||||
const tabs: SingleTabProps<TabId>[] = [
|
||||
{
|
||||
id: WorkflowRunTabId.OUTPUT,
|
||||
title: 'Output',
|
||||
Icon: IconLogout,
|
||||
disabled: isOutputTabDisabled,
|
||||
},
|
||||
{ id: WorkflowRunTabId.NODE, title: 'Node', Icon: IconStepInto },
|
||||
{
|
||||
id: WorkflowRunTabId.INPUT,
|
||||
title: 'Input',
|
||||
Icon: IconLogin2,
|
||||
disabled: areInputAndOutputTabsDisabled,
|
||||
},
|
||||
{
|
||||
id: WorkflowRunTabId.OUTPUT,
|
||||
title: 'Output',
|
||||
Icon: IconLogout,
|
||||
disabled: areInputAndOutputTabsDisabled,
|
||||
disabled: isInputTabDisabled,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isDefined(workflowRun)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowStepContextProvider
|
||||
value={{
|
||||
@ -93,6 +95,13 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === WorkflowRunTabId.NODE ? (
|
||||
<WorkflowRunStepNodeDetail
|
||||
stepId={workflowSelectedNode}
|
||||
@ -108,13 +117,6 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
</StyledContainer>
|
||||
</WorkflowStepContextProvider>
|
||||
);
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
|
||||
export const getIsInputTabDisabled = ({
|
||||
stepExecutionStatus,
|
||||
workflowSelectedNode,
|
||||
}: {
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
}) => {
|
||||
return (
|
||||
workflowSelectedNode === TRIGGER_STEP_ID ||
|
||||
stepExecutionStatus === 'not-executed'
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const getIsOutputTabDisabled = ({
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
}) => {
|
||||
return (
|
||||
stepExecutionStatus === 'running' || stepExecutionStatus === 'not-executed'
|
||||
);
|
||||
};
|
||||
@ -8,7 +8,6 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
|
||||
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
|
||||
import { CardType } from '@/object-record/record-show/types/CardType';
|
||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
||||
import { WorkflowRunOutputVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunOutputVisualizer';
|
||||
import { WorkflowRunVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizer';
|
||||
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
||||
@ -104,7 +103,4 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
||||
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||
</>
|
||||
),
|
||||
[CardType.WorkflowRunOutputCard]: ({ targetableObject }) => (
|
||||
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
|
||||
),
|
||||
};
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
IconHome,
|
||||
IconMail,
|
||||
IconNotes,
|
||||
IconPrinter,
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
@ -189,21 +188,7 @@ export const useRecordShowContainerTabs = (
|
||||
},
|
||||
[CoreObjectNameSingular.WorkflowRun]: {
|
||||
tabs: {
|
||||
workflowRunOutput: {
|
||||
title: 'Output',
|
||||
position: 0,
|
||||
Icon: IconPrinter,
|
||||
cards: [{ type: CardType.WorkflowRunOutputCard }],
|
||||
hide: {
|
||||
ifMobile: false,
|
||||
ifDesktop: false,
|
||||
ifInRightDrawer: false,
|
||||
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
|
||||
ifRequiredObjectsInactive: [],
|
||||
ifRelationsMissing: [],
|
||||
},
|
||||
},
|
||||
workflowRunFlow: {
|
||||
workflowRun: {
|
||||
title: 'Flow',
|
||||
position: 0,
|
||||
Icon: IconSettings,
|
||||
|
||||
@ -9,6 +9,5 @@ export enum CardType {
|
||||
WorkflowCard = 'WorkflowCard',
|
||||
WorkflowVersionCard = 'WorkflowVersionCard',
|
||||
WorkflowRunCard = 'WorkflowRunCard',
|
||||
WorkflowRunOutputCard = 'WorkflowRunOutputCard',
|
||||
RichTextCard = 'RichTextCard',
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { stepsOutputSchemaFamilyState } from '@/workflow/states/stepsOutputSchemaFamilyState';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutputSchemaFamilyStateKey';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
@ -36,17 +35,7 @@ export const useStepsOutputSchema = () => {
|
||||
const trigger = workflowVersion.trigger;
|
||||
|
||||
if (isDefined(trigger)) {
|
||||
const triggerIconKey =
|
||||
trigger.type === 'DATABASE_EVENT'
|
||||
? getTriggerIcon({
|
||||
type: trigger.type,
|
||||
eventName: splitWorkflowTriggerEventName(
|
||||
trigger.settings?.eventName,
|
||||
).event,
|
||||
})
|
||||
: getTriggerIcon({
|
||||
type: trigger.type,
|
||||
});
|
||||
const triggerIconKey = getTriggerIcon(trigger);
|
||||
|
||||
const triggerOutputSchema: StepOutputSchema = {
|
||||
id: TRIGGER_STEP_ID,
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
workflowDatabaseEventTriggerSchema,
|
||||
workflowDeleteRecordActionSchema,
|
||||
workflowDeleteRecordActionSettingsSchema,
|
||||
workflowExecutorOutputSchema,
|
||||
workflowFindRecordsActionSchema,
|
||||
workflowFindRecordsActionSettingsSchema,
|
||||
workflowFormActionSchema,
|
||||
@ -110,6 +111,9 @@ export type WorkflowVersion = {
|
||||
};
|
||||
|
||||
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
|
||||
export type WorkflowExecutorOutput = z.infer<
|
||||
typeof workflowExecutorOutputSchema
|
||||
>;
|
||||
export type WorkflowRunOutputStepsOutput = z.infer<
|
||||
typeof workflowRunOutputStepsOutputSchema
|
||||
>;
|
||||
|
||||
@ -233,7 +233,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [
|
||||
]);
|
||||
|
||||
// Step output schemas
|
||||
const workflowExecutorOutputSchema = z.object({
|
||||
export const workflowExecutorOutputSchema = z.object({
|
||||
result: z.any().optional(),
|
||||
error: z.string().optional(),
|
||||
pendingEvent: z.boolean().optional(),
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import {
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramStepNodeData,
|
||||
WorkflowDiagramRunStatus,
|
||||
WorkflowRunDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
@ -24,9 +27,15 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
||||
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
|
||||
const goBackToFirstWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
|
||||
const resetWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
({
|
||||
workflowSelectedNode,
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||
}) => {
|
||||
const activeWorkflowRunRightDrawerTab = getSnapshotValue(
|
||||
snapshot,
|
||||
activeTabIdComponentState.atomFamily({
|
||||
@ -34,15 +43,40 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
||||
}),
|
||||
) as WorkflowRunTabId | null;
|
||||
|
||||
const isInputTabDisabled = getIsInputTabDisabled({
|
||||
stepExecutionStatus,
|
||||
workflowSelectedNode,
|
||||
});
|
||||
const isOutputTabDisabled = getIsOutputTabDisabled({
|
||||
stepExecutionStatus,
|
||||
});
|
||||
|
||||
if (isNull(activeWorkflowRunRightDrawerTab)) {
|
||||
const defaultTabId = isOutputTabDisabled
|
||||
? WorkflowRunTabId.NODE
|
||||
: WorkflowRunTabId.OUTPUT;
|
||||
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
}),
|
||||
defaultTabId,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
activeWorkflowRunRightDrawerTab === 'input' ||
|
||||
activeWorkflowRunRightDrawerTab === 'output'
|
||||
(isInputTabDisabled &&
|
||||
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) ||
|
||||
(isOutputTabDisabled &&
|
||||
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT)
|
||||
) {
|
||||
set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
}),
|
||||
'node',
|
||||
WorkflowRunTabId.NODE,
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -59,15 +93,8 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
|
||||
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||
|
||||
if (
|
||||
selectedNode.id === TRIGGER_STEP_ID ||
|
||||
selectedNodeData.runStatus === 'not-executed' ||
|
||||
selectedNodeData.runStatus === 'running'
|
||||
) {
|
||||
goBackToFirstWorkflowRunRightDrawerTabIfNeeded();
|
||||
}
|
||||
const selectedNodeData =
|
||||
selectedNode.data as WorkflowRunDiagramStepNodeData;
|
||||
|
||||
if (isDefined(workflowId)) {
|
||||
openWorkflowRunViewStepInCommandMenu(
|
||||
@ -75,14 +102,19 @@ export const WorkflowRunDiagramCanvasEffect = () => {
|
||||
selectedNodeData.name,
|
||||
getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
||||
);
|
||||
|
||||
resetWorkflowRunRightDrawerTabIfNeeded({
|
||||
workflowSelectedNode: selectedNode.id,
|
||||
stepExecutionStatus: selectedNodeData.runStatus,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
setWorkflowSelectedNode,
|
||||
resetWorkflowRunRightDrawerTabIfNeeded,
|
||||
workflowId,
|
||||
getIcon,
|
||||
goBackToFirstWorkflowRunRightDrawerTabIfNeeded,
|
||||
openWorkflowRunViewStepInCommandMenu,
|
||||
getIcon,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import { useWorkflowRunUnsafe } from '@/workflow/hooks/useWorkflowRunUnsafe';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
|
||||
const StyledSourceCodeContainer = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const WorkflowRunOutputVisualizer = ({
|
||||
workflowRunId,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
}) => {
|
||||
const workflowRun = useWorkflowRunUnsafe({ workflowRunId });
|
||||
|
||||
if (!isDefined(workflowRun)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSourceCodeContainer>
|
||||
<CodeEditor
|
||||
value={JSON.stringify(workflowRun.output, null, 2)}
|
||||
language="json"
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
/>
|
||||
</StyledSourceCodeContainer>
|
||||
);
|
||||
};
|
||||
@ -41,6 +41,13 @@ export type WorkflowDiagramStepNodeData =
|
||||
runStatus?: WorkflowDiagramRunStatus;
|
||||
};
|
||||
|
||||
export type WorkflowRunDiagramStepNodeData = Exclude<
|
||||
WorkflowDiagramStepNodeData,
|
||||
'runStatus'
|
||||
> & {
|
||||
runStatus: WorkflowDiagramRunStatus;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramCreateStepNodeData = {
|
||||
nodeType: 'create-step';
|
||||
parentNodeId: string;
|
||||
|
||||
@ -19,25 +19,19 @@ export const getWorkflowDiagramTriggerNode = ({
|
||||
switch (trigger.type) {
|
||||
case 'MANUAL': {
|
||||
triggerDefaultLabel = 'Manual Trigger';
|
||||
triggerIcon = getTriggerIcon({
|
||||
type: 'MANUAL',
|
||||
});
|
||||
triggerIcon = getTriggerIcon(trigger);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'CRON': {
|
||||
triggerDefaultLabel = 'On a Schedule';
|
||||
triggerIcon = getTriggerIcon({
|
||||
type: 'CRON',
|
||||
});
|
||||
triggerIcon = getTriggerIcon(trigger);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'WEBHOOK': {
|
||||
triggerDefaultLabel = 'Webhook';
|
||||
triggerIcon = getTriggerIcon({
|
||||
type: 'WEBHOOK',
|
||||
});
|
||||
triggerIcon = getTriggerIcon(trigger);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -50,10 +44,7 @@ export const getWorkflowDiagramTriggerNode = ({
|
||||
DATABASE_TRIGGER_TYPES.find((item) => item.event === triggerEvent.event)
|
||||
?.defaultLabel ?? '';
|
||||
|
||||
triggerIcon = getTriggerIcon({
|
||||
type: 'DATABASE_EVENT',
|
||||
eventName: triggerEvent.event,
|
||||
});
|
||||
triggerIcon = getTriggerIcon(trigger);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { WorkflowExecutorOutput } from '@/workflow/types/Workflow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -30,24 +34,38 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepOutput = workflowRun.output.stepsOutput[stepId];
|
||||
const stepOutput = workflowRun.output.stepsOutput[stepId] as
|
||||
| WorkflowExecutorOutput
|
||||
| undefined;
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
trigger: workflowRun.output.flow.trigger,
|
||||
steps: workflowRun.output.flow.steps,
|
||||
});
|
||||
if (stepDefinition?.type !== 'action') {
|
||||
throw new Error('The output tab must be rendered with an action step.');
|
||||
if (
|
||||
!isDefined(stepDefinition?.definition) ||
|
||||
!isDefined(stepDefinition.definition.name)
|
||||
) {
|
||||
throw new Error('The step is expected to be properly shaped.');
|
||||
}
|
||||
|
||||
const headerTitle = stepDefinition.definition.name;
|
||||
const headerIcon = getActionIcon(stepDefinition.definition.type);
|
||||
const headerIconColor = getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
});
|
||||
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
|
||||
const headerIcon =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerIcon(stepDefinition.definition)
|
||||
: getActionIcon(stepDefinition.definition.type);
|
||||
const headerIconColor =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerIconColor({ theme })
|
||||
: getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
});
|
||||
const headerType =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerHeaderType(stepDefinition.definition)
|
||||
: i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type));
|
||||
|
||||
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = () => 'red';
|
||||
|
||||
@ -58,12 +76,12 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={i18n._(headerType)}
|
||||
headerType={headerType}
|
||||
/>
|
||||
|
||||
<WorkflowRunStepJsonContainer>
|
||||
<JsonTree
|
||||
value={stepOutput}
|
||||
value={stepOutput ?? t`No output available`}
|
||||
shouldExpandNodeInitially={isTwoFirstDepths}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`Empty Object`}
|
||||
@ -71,7 +89,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
getNodeHighlighting={
|
||||
isDefined(stepOutput.error)
|
||||
isDefined(stepOutput?.error)
|
||||
? setRedHighlightingForEveryNode
|
||||
: undefined
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@ -14,6 +15,10 @@ export const getWorkflowRunStepExecutionStatus = ({
|
||||
return 'not-executed';
|
||||
}
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
const stepOutput = workflowRunOutput.stepsOutput?.[stepId];
|
||||
|
||||
if (isDefined(stepOutput?.error)) {
|
||||
|
||||
@ -6,6 +6,7 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
|
||||
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -48,18 +49,11 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const headerIcon = getTriggerIcon({
|
||||
type: 'CRON',
|
||||
});
|
||||
const headerIcon = getTriggerIcon(trigger);
|
||||
|
||||
const defaultLabel =
|
||||
getTriggerDefaultLabel({
|
||||
type: 'CRON',
|
||||
}) ?? '';
|
||||
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : defaultLabel;
|
||||
|
||||
const headerType = 'Trigger';
|
||||
const defaultLabel = getTriggerDefaultLabel(trigger);
|
||||
const headerTitle = trigger.name ?? defaultLabel;
|
||||
const headerType = getTriggerHeaderType(trigger);
|
||||
|
||||
const onBlur = () => {
|
||||
setErrorMessagesVisible(true);
|
||||
|
||||
@ -12,6 +12,7 @@ import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -105,18 +106,9 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
[systemObjects, searchInputValue],
|
||||
);
|
||||
|
||||
const defaultLabel =
|
||||
getTriggerDefaultLabel({
|
||||
type: 'DATABASE_EVENT',
|
||||
eventName: triggerEvent.event,
|
||||
}) ?? '-';
|
||||
|
||||
const headerIcon = getTriggerIcon({
|
||||
type: 'DATABASE_EVENT',
|
||||
eventName: triggerEvent.event,
|
||||
});
|
||||
|
||||
const headerType = `Trigger · ${defaultLabel}`;
|
||||
const defaultLabel = trigger.name ?? getTriggerDefaultLabel(trigger);
|
||||
const headerIcon = getTriggerIcon(trigger);
|
||||
const headerType = getTriggerHeaderType(trigger);
|
||||
|
||||
const handleOptionClick = (value: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
|
||||
@ -8,11 +8,13 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type WorkflowEditTriggerManualFormProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
@ -48,11 +50,10 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
? 'WHEN_RECORD_SELECTED'
|
||||
: 'EVERYWHERE';
|
||||
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
|
||||
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
|
||||
|
||||
const headerIcon = getTriggerIcon({
|
||||
type: 'MANUAL',
|
||||
});
|
||||
const headerIcon = getTriggerIcon(trigger);
|
||||
const headerType = getTriggerHeaderType(trigger);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -70,7 +71,7 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Manual"
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons, IconCopy } from 'twenty-ui/display';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
|
||||
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
|
||||
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
|
||||
import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput';
|
||||
import { useState } from 'react';
|
||||
import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
|
||||
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconCopy, useIcons } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
type WorkflowEditTriggerWebhookFormProps = {
|
||||
trigger: WorkflowWebhookTrigger;
|
||||
@ -59,11 +61,10 @@ export const WorkflowEditTriggerWebhookForm = ({
|
||||
setErrorMessagesVisible(true);
|
||||
};
|
||||
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Webhook';
|
||||
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
|
||||
|
||||
const headerIcon = getTriggerIcon({
|
||||
type: 'WEBHOOK',
|
||||
});
|
||||
const headerIcon = getTriggerIcon(trigger);
|
||||
const headerType = getTriggerHeaderType(trigger);
|
||||
|
||||
const webhookUrl = `${REACT_APP_SERVER_BASE_URL}/webhooks/workflows/${currentWorkspace?.id}/${workflowId}`;
|
||||
const displayWebhookUrl = webhookUrl.replace(/^(https?:\/\/)?(www\.)?/, '');
|
||||
@ -98,7 +99,7 @@ export const WorkflowEditTriggerWebhookForm = ({
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Trigger · Webhook"
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export const getTriggerHeaderType = (trigger: WorkflowTrigger) => {
|
||||
switch (trigger.type) {
|
||||
case 'CRON': {
|
||||
return 'Trigger';
|
||||
}
|
||||
case 'WEBHOOK': {
|
||||
return 'Trigger · Webhook';
|
||||
}
|
||||
case 'MANUAL': {
|
||||
return 'Trigger · Manual';
|
||||
}
|
||||
case 'DATABASE_EVENT': {
|
||||
const defaultLabel = getTriggerDefaultLabel(trigger);
|
||||
|
||||
return `Trigger · ${defaultLabel}`;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(trigger, 'Unknown trigger type');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,26 +1,18 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
|
||||
|
||||
export const getTriggerIcon = (
|
||||
trigger:
|
||||
| {
|
||||
type: 'MANUAL';
|
||||
}
|
||||
| {
|
||||
type: 'CRON';
|
||||
}
|
||||
| {
|
||||
type: 'WEBHOOK';
|
||||
}
|
||||
| {
|
||||
type: 'DATABASE_EVENT';
|
||||
eventName: string;
|
||||
},
|
||||
trigger: WorkflowTrigger,
|
||||
): string | undefined => {
|
||||
if (trigger.type === 'DATABASE_EVENT') {
|
||||
return DATABASE_TRIGGER_TYPES.find(
|
||||
(type) => type.event === trigger.eventName,
|
||||
)?.icon;
|
||||
const eventName = splitWorkflowTriggerEventName(
|
||||
trigger.settings.eventName,
|
||||
).event;
|
||||
|
||||
return DATABASE_TRIGGER_TYPES.find((type) => type.event === eventName)
|
||||
?.icon;
|
||||
}
|
||||
|
||||
return OTHER_TRIGGER_TYPES.find((item) => item.type === trigger.type)?.icon;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { Theme } from '@emotion/react';
|
||||
|
||||
export const getTriggerIconColor = ({ theme }: { theme: Theme }) => {
|
||||
return theme.font.color.tertiary;
|
||||
};
|
||||
@ -1,28 +1,33 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getTriggerDefaultLabel = (
|
||||
trigger:
|
||||
| {
|
||||
type: 'MANUAL';
|
||||
}
|
||||
| {
|
||||
type: 'CRON';
|
||||
}
|
||||
| {
|
||||
type: 'WEBHOOK';
|
||||
}
|
||||
| {
|
||||
type: 'DATABASE_EVENT';
|
||||
eventName: string;
|
||||
},
|
||||
): string | undefined => {
|
||||
export const getTriggerDefaultLabel = (trigger: WorkflowTrigger): string => {
|
||||
if (trigger.type === 'DATABASE_EVENT') {
|
||||
return DATABASE_TRIGGER_TYPES.find(
|
||||
(type) => type.event === trigger.eventName,
|
||||
const triggerEvent = splitWorkflowTriggerEventName(
|
||||
trigger.settings.eventName,
|
||||
);
|
||||
|
||||
const label = DATABASE_TRIGGER_TYPES.find(
|
||||
(type) => type.event === triggerEvent.event,
|
||||
)?.defaultLabel;
|
||||
|
||||
if (!isDefined(label)) {
|
||||
throw new Error('Unknown trigger event');
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
return OTHER_TRIGGER_TYPES.find((item) => item.type === trigger.type)
|
||||
?.defaultLabel;
|
||||
const label = OTHER_TRIGGER_TYPES.find(
|
||||
(item) => item.type === trigger.type,
|
||||
)?.defaultLabel;
|
||||
|
||||
if (!isDefined(label)) {
|
||||
throw new Error('Unknown trigger type');
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
@ -28,11 +28,7 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
|
||||
const getDatabaseEventTriggerStepName = (
|
||||
trigger: WorkflowDatabaseEventTrigger,
|
||||
): string => {
|
||||
const [, action] = trigger.settings.eventName.split('.');
|
||||
const defaultLabel = getTriggerDefaultLabel({
|
||||
type: 'DATABASE_EVENT',
|
||||
eventName: action,
|
||||
});
|
||||
const defaultLabel = getTriggerDefaultLabel(trigger);
|
||||
|
||||
return defaultLabel ?? '';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user