diff --git a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx
index 568f69663..01f2c0d4f 100644
--- a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx
+++ b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx
@@ -14,18 +14,19 @@ import SortableWorkflowsContext from './components/SortableWorkflowsContext';
type ContentProps = {
id: string;
+ basedOn?: string;
isCollapsable?: boolean;
containerProps?: CardProps;
};
-const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: ContentProps) => {
- const { onCreateWorkflow, onChainWorkflow, onEditWorkflow, onRemoveWorkflow } = useWorkflowActions();
- const workflow = useWorkflow(id);
+const WorkflowCardContent = memo(({ id, basedOn, isCollapsable, containerProps }: ContentProps) => {
+ const workflowId = basedOn || id;
+
const containerRef = useRef(null);
+ const workflow = useWorkflow(workflowId);
const { data: stacksAndMachines } = useStacksAndMachines();
- const { isOpen, onOpen, onToggle } = useDisclosure({
- defaultIsOpen: !isCollapsable,
- });
+ const { isOpen, onOpen, onToggle } = useDisclosure({ defaultIsOpen: !isCollapsable });
+ const { onCreateWorkflow, onChainWorkflow, onEditWorkflow, onRemoveWorkflow } = useWorkflowActions();
const { isSelected } = useSelection();
const isHighlighted = isSelected(id);
@@ -73,10 +74,10 @@ const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: Content
- {workflow.userValues.title || id}
+ {basedOn ? id : workflow.userValues.title || id}
- {stack.name || 'Unknown stack'}
+ {basedOn ? `Based on ${basedOn}` : stack.name || 'Unknown stack'}
@@ -125,9 +126,13 @@ const WorkflowCardContent = memo(({ id, isCollapsable, containerProps }: Content
- before_run`} placement="before_run" parentWorkflowId={id} />
-
- after_run`} placement="after_run" parentWorkflowId={id} />
+ before_run`}
+ placement="before_run"
+ parentWorkflowId={workflowId}
+ />
+
+ after_run`} placement="after_run" parentWorkflowId={workflowId} />
@@ -143,6 +148,7 @@ type Props = ContentProps & WorkflowActions & StepActions & Selection;
const WorkflowCard = ({
id,
+ basedOn,
isCollapsable,
containerProps,
selectedWorkflowId = '',
@@ -154,7 +160,7 @@ const WorkflowCard = ({
selectedStepIndex={selectedStepIndex}
{...actions}
>
-
+
);
diff --git a/source/javascripts/core/models/BitriseYml.schema.ts b/source/javascripts/core/models/BitriseYml.schema.ts
index 0772fc42d..a6570c13a 100644
--- a/source/javascripts/core/models/BitriseYml.schema.ts
+++ b/source/javascripts/core/models/BitriseYml.schema.ts
@@ -266,26 +266,7 @@ const BitriseYmlSchema = {
workflows: {
patternProperties: {
'.*': {
- properties: {
- depends_on: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- abort_on_fail: {
- type: 'boolean',
- },
- should_always_run: {
- type: 'string',
- enum: ['off', 'workflow'],
- },
- run_if: {
- $ref: '#/definitions/RunIfModel',
- },
- },
- additionalProperties: false,
- type: 'object',
+ $ref: '#/definitions/GraphPipelineWorkflowModel',
},
},
type: 'object',
@@ -306,6 +287,41 @@ const BitriseYmlSchema = {
additionalProperties: false,
type: 'object',
},
+ GraphPipelineWorkflowModel: {
+ properties: {
+ based_on: {
+ type: 'string',
+ },
+ depends_on: {
+ items: {
+ type: 'string',
+ },
+ type: 'array',
+ },
+ abort_on_fail: {
+ type: 'boolean',
+ },
+ should_always_run: {
+ type: 'string',
+ enum: ['off', 'workflow'],
+ },
+ run_if: {
+ $ref: '#/definitions/GraphPipelineWorkflowRunIfModel',
+ },
+ },
+ additionalProperties: false,
+ type: 'object',
+ },
+ GraphPipelineWorkflowRunIfModel: {
+ required: ['expression'],
+ properties: {
+ expression: {
+ type: 'string',
+ },
+ },
+ additionalProperties: false,
+ type: 'object',
+ },
StageModel: {
properties: {
title: {
diff --git a/source/javascripts/core/models/BitriseYmlService.ts b/source/javascripts/core/models/BitriseYmlService.ts
index ed0327d9f..4fd190cbd 100644
--- a/source/javascripts/core/models/BitriseYmlService.ts
+++ b/source/javascripts/core/models/BitriseYmlService.ts
@@ -479,7 +479,7 @@ function addWorkflowToPipeline(
}
if (parentWorkflowId) {
- if (!copy.workflows?.[parentWorkflowId] || !copy.pipelines?.[pipelineId]?.workflows?.[parentWorkflowId]) {
+ if (!copy.pipelines?.[pipelineId]?.workflows?.[parentWorkflowId]) {
return copy;
}
}
diff --git a/source/javascripts/core/models/Workflow.ts b/source/javascripts/core/models/Workflow.ts
index 632a9e4d6..fad7b299e 100644
--- a/source/javascripts/core/models/Workflow.ts
+++ b/source/javascripts/core/models/Workflow.ts
@@ -7,6 +7,6 @@ type WorkflowYmlObject = Workflows[string] & {
};
type Workflow = { id: string; userValues: WorkflowYmlObject };
type ChainedWorkflowPlacement = 'before_run' | 'after_run';
-type PipelineWorkflow = { id: string; dependsOn: string[] };
+type PipelineWorkflow = { id: string; basedOn?: string; dependsOn: string[] };
export { Workflow, WorkflowYmlObject, Workflows, ChainedWorkflowPlacement, PipelineWorkflow };
diff --git a/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx b/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx
index 1b8b59746..76af54ea4 100644
--- a/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx
+++ b/source/javascripts/pages/PipelinesPage/PipelinesPage.stories.tsx
@@ -1,5 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import { Box } from '@bitrise/bitkit';
+import { cloneDeep } from 'es-toolkit';
import PipelinesPage from './PipelinesPage';
export default {
@@ -116,3 +117,22 @@ export const ReactivatePlan: Story = {
};
export const GraphPipelineWithEditing: Story = {};
+
+const withWorkflowOverrideYml = () => {
+ const yml = cloneDeep(TEST_BITRISE_YML);
+
+ if (yml.pipelines?.['graph-pipeline']?.workflows) {
+ yml.pipelines['graph-pipeline'].workflows.override = {
+ based_on: 'wf3',
+ depends_on: ['wf1'],
+ };
+ }
+
+ return yml;
+};
+
+export const WithWorkflowOverride: Story = {
+ args: {
+ yml: withWorkflowOverrideYml(),
+ },
+};
diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts
index 19ef70b1e..c4da31c32 100644
--- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts
+++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/GraphPipelineCanvas.types.ts
@@ -1,7 +1,7 @@
import { Edge, Node } from '@xyflow/react';
import { PLACEHOLDER_NODE_TYPE, WORKFLOW_NODE_TYPE } from './GraphPipelineCanvas.const';
-export type WorkflowNodeType = Node<{ fixed?: boolean; highlighted?: boolean }>;
+export type WorkflowNodeType = Node<{ basedOn?: string; fixed?: boolean; highlighted?: boolean }>;
export type PlaceholderNodeType = Node<{ dependsOn: string[] }>;
export type GraphPipelineEdgeType = Edge<{ highlighted?: boolean }>;
export type GraphPipelineNodeType = PlaceholderNodeType | WorkflowNodeType;
diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx
index 6c7c1a0d8..780c16452 100644
--- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx
+++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/components/WorkflowNode.tsx
@@ -32,18 +32,19 @@ const selectedStyle = {
outlineOffset: '-2px',
} satisfies CardProps;
-const WorkflowNode = ({ id, zIndex, selected }: Props) => {
+const WorkflowNode = ({ id, selected, zIndex, data }: Props) => {
const ref = useRef(null);
const hovered = useHover(ref);
const workflows = useWorkflows();
+ const { selectedPipeline } = usePipelineSelector();
+
const openDialog = usePipelinesPageStore((s) => s.openDialog);
const closeDialog = usePipelinesPageStore((s) => s.closeDialog);
- const selectedWorkflowId = usePipelinesPageStore((s) => s.workflowId);
- const selectedStepIndex = usePipelinesPageStore((s) => s.stepIndex);
const setStepIndex = usePipelinesPageStore((s) => s.setStepIndex);
-
- const { selectedPipeline } = usePipelineSelector();
+ const selectedStepIndex = usePipelinesPageStore((s) => s.stepIndex);
+ const selectedWorkflowId = usePipelinesPageStore((s) => s.workflowId);
const isGraphPipelinesEnabled = useFeatureFlag('enable-dag-pipelines');
+
const { updateNode, deleteElements, setEdges } = useReactFlow();
const { moveStep, cloneStep, deleteStep, upgradeStep, setChainedWorkflows, removeChainedWorkflow } =
@@ -61,6 +62,8 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => {
onResize: ({ height }) => updateNode(id, { height }),
});
+ const basedOn = 'basedOn' in data ? data.basedOn : undefined;
+
const {
handleAddStep,
handleMoveStep,
@@ -131,6 +134,15 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => {
}
}
+ if (basedOn) {
+ return {
+ handleRemoveWorkflow: (deletedWorkflowId: string) => {
+ deleteElements({ nodes: [{ id: deletedWorkflowId }] });
+ handleWorkflowActionDialogChange(deletedWorkflowId, 'remove');
+ },
+ };
+ }
+
return {
handleAddStep: (workflowId: string, stepIndex: number) =>
openDialog({
@@ -206,21 +218,22 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => {
},
};
}, [
+ basedOn,
+ workflows,
+ selectedPipeline,
+ selectedStepIndex,
+ selectedWorkflowId,
isGraphPipelinesEnabled,
moveStep,
cloneStep,
upgradeStep,
openDialog,
- selectedPipeline,
deleteStep,
- selectedWorkflowId,
- selectedStepIndex,
closeDialog,
setStepIndex,
deleteElements,
- removeChainedWorkflow,
- workflows,
setChainedWorkflows,
+ removeChainedWorkflow,
]);
const containerProps = useMemo(
@@ -252,9 +265,10 @@ const WorkflowNode = ({ id, zIndex, selected }: Props) => {
{
return Object.entries(pipelineWorkflows).map(([id, pipelineWorkflow]) => {
return {
id,
+ basedOn: pipelineWorkflow.based_on,
dependsOn: pipelineWorkflow.depends_on ?? [],
} satisfies PipelineWorkflow;
});
diff --git a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts
index 90ab0020e..9b02e6fe7 100644
--- a/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts
+++ b/source/javascripts/pages/PipelinesPage/components/PipelineCanvas/GraphPipelineCanvas/utils/createWorkflowNode.ts
@@ -10,7 +10,7 @@ import { GraphPipelineNodeType } from '../GraphPipelineCanvas.types';
function createWorkflowNode(workflow: PipelineWorkflow, actionable: boolean) {
return {
id: workflow.id,
- data: {},
+ data: { basedOn: workflow.basedOn },
deletable: actionable,
draggable: false,
focusable: false,