From cdfd70ea57b9e1cb7a6ecb98b7f195fdb2b04a2b Mon Sep 17 00:00:00 2001 From: Zoltan Szabo <63643463+zoltanszabo-bitrise@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:46:53 +0200 Subject: [PATCH] feat: Handle step bundle and with group selection (#1205) --- package-lock.json | 2 + source/javascripts/_componentRegister.js | 392 +++++++----------- .../DiffEditor/DiffEditorDialog.stories.tsx | 1 + .../StepBundle/StepBundleDrawer.stories.tsx | 17 + .../StepBundle/StepBundleDrawer.tsx | 62 +++ .../StepBundlePanel.stories.tsx | 8 +- .../StepBundle/StepBundlePanel.tsx | 47 +++ .../StepBundlePanel/StepBundlePanel.tsx | 36 -- .../StepConfigDrawer.context.tsx | 20 +- .../StepConfigDrawer/StepConfigDrawer.tsx | 4 +- .../StepSelectorDrawer.utils.ts | 2 +- .../components/VirtualizedRow.tsx | 4 +- .../hooks/useSearchSteps.ts | 13 +- .../hooks/useVirtualizedItems.ts | 2 +- .../WithBlockPanel/WithBlockPanel.stories.tsx | 15 - .../WithBlockPanel/WithBlockPanel.tsx | 59 --- .../WithGroup/WithGroupDrawer.stories.tsx | 17 + .../WithGroup/WithGroupDrawer.tsx | 66 +++ .../WithGroup/WithGroupPanel.stories.tsx | 14 + .../WithGroup/WithGroupPanel.tsx | 71 ++++ .../WorkflowCard/WorkflowCard.tsx | 38 +- .../WorkflowCard/WorkflowCard.types.ts | 20 +- .../components/ChainedWorkflowCard.tsx | 32 +- .../components/ChainedWorkflowList.tsx | 23 +- .../components/SortableWorkflowsContext.tsx | 5 +- .../WorkflowCard/components/StepCard.tsx | 43 +- .../WorkflowCard/components/StepList.tsx | 16 +- .../components/unified-editor/index.ts | 10 +- .../controllers/_WorkflowsController.js.erb | 2 +- .../javascripts/core/api/StepApi.mswMocks.ts | 32 +- source/javascripts/core/api/StepApi.ts | 40 +- .../core/models/BitriseYml.mocks.ts | 43 +- source/javascripts/core/models/Step.ts | 34 +- .../core/models/StepService.spec.ts | 60 ++- source/javascripts/core/models/StepService.ts | 21 +- source/javascripts/hooks/useEnvVars.ts | 2 +- source/javascripts/hooks/useStep.ts | 138 +++--- .../WorkflowsPage/WorkflowsPage.store.ts | 20 +- .../WorkflowsPage/WorkflowsPage.stories.tsx | 11 +- .../pages/WorkflowsPage/WorkflowsPage.tsx | 26 +- .../WorkflowCanvasPanel.tsx | 41 +- .../javascripts/pages/WorkflowsPage/index.ts | 6 +- source/templates/workflows.slim | 2 +- 43 files changed, 909 insertions(+), 608 deletions(-) create mode 100644 source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.stories.tsx create mode 100644 source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.tsx rename source/javascripts/components/unified-editor/{StepBundlePanel => StepBundle}/StepBundlePanel.stories.tsx (67%) create mode 100644 source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.tsx delete mode 100644 source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.tsx delete mode 100644 source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.stories.tsx delete mode 100644 source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.tsx create mode 100644 source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.stories.tsx create mode 100644 source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.tsx create mode 100644 source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.stories.tsx create mode 100644 source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.tsx diff --git a/package-lock.json b/package-lock.json index 9dc4de2a8..521ace039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22595,6 +22595,8 @@ }, "node_modules/msw-storybook-addon": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.3.tgz", + "integrity": "sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/source/javascripts/_componentRegister.js b/source/javascripts/_componentRegister.js index 6b2072ba6..097d699fe 100644 --- a/source/javascripts/_componentRegister.js +++ b/source/javascripts/_componentRegister.js @@ -1,20 +1,20 @@ -import { react2angular } from "@bitrise/react2angular"; -import { Checkbox, Icon } from "@bitrise/bitkit"; - -import Header from "./components/Header"; -import InfoTooltip from "./components/InfoTooltip"; -import Navigation from "./components/Navigation"; -import Notification from "./components/Notification"; -import NotificationMessageWithLink from "./components/NotificationMessageWithLink"; -import StepBadge from "./components/StepBadge"; -import Toggle from "./components/Toggle"; -import UpdateConfigurationDialog from "./components/UpdateConfigurationDialog/UpdateConfigurationDialog"; -import WorkflowRecipesInfoBanner from "./components/WorkflowRecipesInfoBanner"; -import YmlEditor from "./components/YmlEditor/YmlEditor"; -import YmlEditorHeader from "./components/YmlEditorHeader/YmlEditorHeader"; -import DiffEditorDialog from "@/components/DiffEditor/DiffEditorDialog"; -import { RootComponent, withRootProvider } from "./utils/withRootProvider"; +import { react2angular } from '@bitrise/react2angular'; +import { Checkbox, Icon } from '@bitrise/bitkit'; +import Header from './components/Header'; +import InfoTooltip from './components/InfoTooltip'; +import Navigation from './components/Navigation'; +import Notification from './components/Notification'; +import NotificationMessageWithLink from './components/NotificationMessageWithLink'; +import StepBadge from './components/StepBadge'; +import Toggle from './components/Toggle'; +import UpdateConfigurationDialog from './components/UpdateConfigurationDialog/UpdateConfigurationDialog'; +import WorkflowRecipesInfoBanner from './components/WorkflowRecipesInfoBanner'; +import YmlEditor from './components/YmlEditor/YmlEditor'; +import YmlEditorHeader from './components/YmlEditorHeader/YmlEditorHeader'; +import DiffEditorDialog from './components/DiffEditor/DiffEditorDialog'; +import { StepBundlePanel, StepSelectorDrawer, WithGroupPanel, WorkflowEmptyState } from './components/unified-editor'; +import { RootComponent, withRootProvider } from './utils/withRootProvider'; import { ChainWorkflowDrawer, CreateWorkflowDialog, @@ -24,19 +24,8 @@ import { VersionChangeDialog, WorkflowConfigPanel, WorkflowToolbar, -} from "@/pages/WorkflowsPage"; -import { - PipelinesPage, - SecretsPage, - TriggersPage, - WorkflowsPage, -} from "@/pages"; -import { - StepBundlePanel, - StepSelectorDrawer, - WithBlockPanel, - WorkflowEmptyState, -} from "@/components/unified-editor"; +} from './pages/WorkflowsPage'; +import { PipelinesPage, SecretsPage, TriggersPage, WorkflowsPage } from './pages'; function register(component, props, injects) { return react2angular(withRootProvider(component), props, injects); @@ -44,248 +33,175 @@ function register(component, props, injects) { // Page components angular - .module("BitriseWorkflowEditor") + .module('BitriseWorkflowEditor') .component( - "rTriggersPage", + 'rTriggersPage', register(TriggersPage, [ - "onTriggerMapChange", - "pipelines", - "triggerMap", - "setDiscard", - "workflows", - "isWebsiteMode", - "integrationsUrl", + 'onTriggerMapChange', + 'pipelines', + 'triggerMap', + 'setDiscard', + 'workflows', + 'isWebsiteMode', + 'integrationsUrl', ]), ) .component( - "rSecretsPage", + 'rSecretsPage', register(SecretsPage, [ - "secrets", - "secretsWriteNew", - "onSecretsChange", - "getSecretValue", - "appSlug", - "secretSettingsUrl", - "sharedSecretsAvailable", - "planSelectorPageUrl", - ]), - ) - .component("rPipelinesPage", register(PipelinesPage, ["yml"])) - .component("rWorkflowsPage", register(WorkflowsPage, ["yml", "onChange"])); + 'secrets', + 'secretsWriteNew', + 'onSecretsChange', + 'getSecretValue', + 'appSlug', + 'secretSettingsUrl', + 'sharedSecretsAvailable', + 'planSelectorPageUrl', + ]), + ) + .component('rPipelinesPage', register(PipelinesPage, ['yml'])) + .component('rWorkflowsPage', register(WorkflowsPage, ['yml', 'onChange'])); // Components angular - .module("BitriseWorkflowEditor") + .module('BitriseWorkflowEditor') + .component('rNotification', register(Notification, ['message', 'title', 'status'])) .component( - "rNotification", - register(Notification, ["message", "title", "status"]), + 'rNotificationMessageWithLink', + register(NotificationMessageWithLink, ['message', 'type', 'linkUrl', 'linkText']), ) + .component('rCheckbox', register(Checkbox, ['children', 'isDisabled'])) + .component('rRootComponent', react2angular(RootComponent)) + .component('rIcon', register(Icon, ['name', 'textColor', 'size'])) .component( - "rNotificationMessageWithLink", - register(NotificationMessageWithLink, [ - "message", - "type", - "linkUrl", - "linkText", - ]), - ) - .component("rCheckbox", register(Checkbox, ["children", "isDisabled"])) - .component("rRootComponent", react2angular(RootComponent)) - .component("rIcon", register(Icon, ["name", "textColor", "size"])) - .component( - "rStepItem", + 'rStepItem', register(StepItem, [ - "workflowIndex", - "step", - "displayName", - "version", - "hasVersionUpdate", - "isSelected", - "onSelected", + 'workflowIndex', + 'step', + 'displayName', + 'version', + 'hasVersionUpdate', + 'isSelected', + 'onSelected', ]), ) - .component("rStepItemBadge", register(StepBadge, ["step"])) + .component('rStepItemBadge', register(StepBadge, ['step'])) .component( - "rUpdateConfigurationDialog", + 'rUpdateConfigurationDialog', register(UpdateConfigurationDialog, [ - "onClose", - "appSlug", - "getDataToSave", - "onComplete", - "defaultBranch", - "gitRepoSlug", + 'onClose', + 'appSlug', + 'getDataToSave', + 'onComplete', + 'defaultBranch', + 'gitRepoSlug', ]), ) .component( - "rYmlEditorHeader", + 'rYmlEditorHeader', register(YmlEditorHeader, [ - "url", - "initialUsesRepositoryYml", - "appSlug", - "appConfig", - "onUsesRepositoryYmlChangeSaved", - "repositoryYmlAvailable", - "isWebsiteMode", - "defaultBranch", - "gitRepoSlug", - "lines", - "split", - "modularYamlSupported", - "lastModified", - ]), - ) - .component( - "rYmlEditor", - register(YmlEditor, ["yml", "readonly", "onChange", "isLoading"]), - ) - .component( - "rWorkflowToolbar", + 'url', + 'initialUsesRepositoryYml', + 'appSlug', + 'appConfig', + 'onUsesRepositoryYmlChangeSaved', + 'repositoryYmlAvailable', + 'isWebsiteMode', + 'defaultBranch', + 'gitRepoSlug', + 'lines', + 'split', + 'modularYamlSupported', + 'lastModified', + ]), + ) + .component('rYmlEditor', register(YmlEditor, ['yml', 'readonly', 'onChange', 'isLoading'])) + .component( + 'rWorkflowToolbar', register(WorkflowToolbar, [ - "workflows", - "selectedWorkflow", - "selectWorkflow", - "createWorkflow", - "chainWorkflow", - "deleteWorkflow", - "rearrangeWorkflows", - "uniqueStepCount", - "canRunWorkflow", - "isRunWorkflowDisabled", - ]), - ) - .component( - "rWorkflowEmptyState", - register(WorkflowEmptyState, ["onCreateWorkflow"]), - ) - .component( - "rWorkflowRecipesInfoBanner", - register(WorkflowRecipesInfoBanner, []), - ) - .component("rInfoTooltip", register(InfoTooltip, ["label"])) - .component( - "rToggle", - register(Toggle, [ - "tooltipLabel", - "isDisabled", - "isChecked", - "onChange", - "listItemId", - ]), - ) - .component( - "rHeader", + 'workflows', + 'selectedWorkflow', + 'selectWorkflow', + 'createWorkflow', + 'chainWorkflow', + 'deleteWorkflow', + 'rearrangeWorkflows', + 'uniqueStepCount', + 'canRunWorkflow', + 'isRunWorkflowDisabled', + ]), + ) + .component('rWorkflowEmptyState', register(WorkflowEmptyState, ['onCreateWorkflow'])) + .component('rWorkflowRecipesInfoBanner', register(WorkflowRecipesInfoBanner, [])) + .component('rInfoTooltip', register(InfoTooltip, ['label'])) + .component('rToggle', register(Toggle, ['tooltipLabel', 'isDisabled', 'isChecked', 'onChange', 'listItemId'])) + .component( + 'rHeader', register(Header, [ - "appName", - "appPath", - "workspacePath", - "workflowsAndPipelinesPath", - "isDiffEditorEnabled", - "onDiffClick", - "isDiffDisabled", - "onSaveClick", - "isSaveDisabled", - "isSaveInProgress", - "onDiscardClick", - "isDiscardDisabled", - "isWebsiteMode", - ]), - ) - .component( - "rDiffDialog", - register(DiffEditorDialog, [ - "isOpen", - "onClose", - "originalText", - "modifiedText", - "onChange", + 'appName', + 'appPath', + 'workspacePath', + 'workflowsAndPipelinesPath', + 'isDiffEditorEnabled', + 'onDiffClick', + 'isDiffDisabled', + 'onSaveClick', + 'isSaveDisabled', + 'isSaveInProgress', + 'onDiscardClick', + 'isDiscardDisabled', + 'isWebsiteMode', ]), ) .component( - "rNavigation", - register(Navigation, ["items", "activeItem", "onItemSelected"]), + 'rDiffDialog', + register(DiffEditorDialog, ['isOpen', 'onClose', 'originalText', 'modifiedText', 'onChange']), ) + .component('rNavigation', register(Navigation, ['items', 'activeItem', 'onItemSelected'])) .component( - "rStepConfig", + 'rStepConfig', register(StepConfigPanel, [ - "step", - "tabId", - "environmentVariables", - "secrets", - "resolvedVersion", - "hasVersionUpdate", - "versionsWithRemarks", - "inputCategories", - "outputVariables", - "onChange", - "onClone", - "onRemove", - "onChangeTabId", - "onCreateSecret", - "onLoadSecrets", - "onCreateEnvVar", - "onLoadEnvVars", - "secretsWriteNew", - ]), - ) - .component( - "rVersionChangeDialog", + 'step', + 'tabId', + 'environmentVariables', + 'secrets', + 'resolvedVersion', + 'hasVersionUpdate', + 'versionsWithRemarks', + 'inputCategories', + 'outputVariables', + 'onChange', + 'onClone', + 'onRemove', + 'onChangeTabId', + 'onCreateSecret', + 'onLoadSecrets', + 'onCreateEnvVar', + 'onLoadEnvVars', + 'secretsWriteNew', + ]), + ) + .component( + 'rVersionChangeDialog', register(VersionChangeDialog, [ - "isOpen", - "onClose", - "isMajorChange", - "newInputs", - "removedInputs", - "releaseNotesUrl", - ]), - ) - .component( - "rWorkflowConfigPanel", - register(WorkflowConfigPanel, [ - "appSlug", - "yml", - "defaultValues", - "onChange", - ]), - ) - .component( - "rStepSelectorDrawer", - register(StepSelectorDrawer, [ - "isOpen", - "onClose", - "enabledSteps", - "onSelectStep", - ]), - ) - .component( - "rChainWorkflowDrawer", - register(ChainWorkflowDrawer, [ - "workflowId", - "yml", - "isOpen", - "onClose", - "onChainWorkflow", + 'isOpen', + 'onClose', + 'isMajorChange', + 'newInputs', + 'removedInputs', + 'releaseNotesUrl', ]), ) + .component('rWorkflowConfigPanel', register(WorkflowConfigPanel, ['appSlug', 'yml', 'defaultValues', 'onChange'])) + .component('rStepSelectorDrawer', register(StepSelectorDrawer, ['isOpen', 'onClose', 'enabledSteps', 'onSelectStep'])) .component( - "rDeleteWorkflowDialog", - register(DeleteWorkflowDialog, [ - "workflowId", - "isOpen", - "onClose", - "onDeleteWorkflow", - ]), + 'rChainWorkflowDrawer', + register(ChainWorkflowDrawer, ['workflowId', 'yml', 'isOpen', 'onClose', 'onChainWorkflow']), ) .component( - "rCreateWorkflowDialog", - register(CreateWorkflowDialog, [ - "yml", - "isOpen", - "onClose", - "onCreateWorkflow", - ]), + 'rDeleteWorkflowDialog', + register(DeleteWorkflowDialog, ['workflowId', 'isOpen', 'onClose', 'onDeleteWorkflow']), ) - .component("rStepBundlePanel", register(StepBundlePanel, ["bundleName"])) - .component( - "rWithBlockPanel", - register(WithBlockPanel, ["groupName", "imageName", "services"]), - ); + .component('rCreateWorkflowDialog', register(CreateWorkflowDialog, ['yml', 'isOpen', 'onClose', 'onCreateWorkflow'])) + .component('rStepBundlePanel', register(StepBundlePanel, ['bundleName'])) + .component('rWithGroupPanel', register(WithGroupPanel, ['groupName', 'imageName', 'services'])); diff --git a/source/javascripts/components/DiffEditor/DiffEditorDialog.stories.tsx b/source/javascripts/components/DiffEditor/DiffEditorDialog.stories.tsx index 845b0b671..e1399f0ee 100644 --- a/source/javascripts/components/DiffEditor/DiffEditorDialog.stories.tsx +++ b/source/javascripts/components/DiffEditor/DiffEditorDialog.stories.tsx @@ -26,6 +26,7 @@ export default { onChange: { action: 'onChange' }, }, render: (args: any) => { + // eslint-disable-next-line react-hooks/rules-of-hooks const { isOpen, onOpen, onClose } = useDisclosure(); return ( diff --git a/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.stories.tsx b/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.stories.tsx new file mode 100644 index 000000000..f2d7e1b45 --- /dev/null +++ b/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { withBitriseYml } from '@/contexts/BitriseYmlProvider'; +import { MockYml } from '@/core/models/BitriseYml.mocks'; +import StepBundleDrawer from './StepBundleDrawer'; + +export default { + title: 'javascripts/components/unified-editor/StepBundle', + component: StepBundleDrawer, + args: { + isOpen: true, + workflowId: 'step-bundle', + stepIndex: 0, + }, + decorators: [(Story) => withBitriseYml(MockYml, Story)], +} as Meta; + +export const Drawer: StoryObj = {}; diff --git a/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.tsx b/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.tsx new file mode 100644 index 000000000..b818f41e0 --- /dev/null +++ b/source/javascripts/components/unified-editor/StepBundle/StepBundleDrawer.tsx @@ -0,0 +1,62 @@ +import { Icon, useDisclosure } from '@bitrise/bitkit'; +import { + Drawer, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + UseDisclosureProps, +} from '@chakra-ui/react'; +import useStep from '@/hooks/useStep'; +import StepService from '@/core/models/StepService'; +import { StepBundleContent, StepBundleHeader } from './StepBundlePanel'; + +type Props = UseDisclosureProps & { + workflowId: string; + stepIndex: number; +}; + +const StepBundleDrawer = ({ workflowId, stepIndex, ...disclosureProps }: Props) => { + const { isOpen, onClose } = useDisclosure(disclosureProps); + const { data } = useStep(workflowId, stepIndex); + const isStepBundle = StepService.isStepBundle(data?.cvs || '', data?.userValues); + + if (!isStepBundle || !data) { + return null; + } + + const { title } = data; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default StepBundleDrawer; diff --git a/source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.stories.tsx b/source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.stories.tsx similarity index 67% rename from source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.stories.tsx rename to source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.stories.tsx index 241b37d20..77b838582 100644 --- a/source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.stories.tsx +++ b/source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.stories.tsx @@ -2,11 +2,11 @@ import { Meta, StoryObj } from '@storybook/react'; import StepBundlePanel from './StepBundlePanel'; export default { + title: 'javascripts/components/unified-editor/StepBundle', component: StepBundlePanel, -} as Meta; - -export const StepBundle: StoryObj = { args: { bundleName: 'Step bundle: install_deps', }, -}; +} as Meta; + +export const Panel: StoryObj = {}; diff --git a/source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.tsx b/source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.tsx new file mode 100644 index 000000000..186a1e4ab --- /dev/null +++ b/source/javascripts/components/unified-editor/StepBundle/StepBundlePanel.tsx @@ -0,0 +1,47 @@ +import { Box, Notification, Text } from '@bitrise/bitkit'; +import useNavigation from '@/hooks/useNavigation'; + +const StepBundleHeader = ({ title }: { title: string }) => ( + + + {title} + + +); + +const StepBundleContent = () => { + const { replace } = useNavigation(); + + return ( + + replace('/yml'), + }} + status="info" + > + Edit step bundle configuration + + View more details or edit step bundle configuration in the Configuration YAML page. + + + + ); +}; + +type StepBundlePanelProps = { + bundleName: string; +}; + +const StepBundlePanel = ({ bundleName }: StepBundlePanelProps) => { + return ( + + + + + ); +}; + +export { StepBundleHeader, StepBundleContent }; +export default StepBundlePanel; diff --git a/source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.tsx b/source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.tsx deleted file mode 100644 index 72bc735d8..000000000 --- a/source/javascripts/components/unified-editor/StepBundlePanel/StepBundlePanel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Box, Notification, Text } from '@bitrise/bitkit'; -import useNavigation from '@/hooks/useNavigation'; - -type StepBundlePanelProps = { - bundleName: string; -}; - -const StepBundlePanel = ({ bundleName }: StepBundlePanelProps) => { - const { replace } = useNavigation(); - - return ( - - - - {bundleName} - - - - replace('/yml'), - }} - status="info" - > - Edit step bundle configuration - - View more details or edit step bundle configuration in the Configuration YAML page. - - - - - ); -}; - -export default StepBundlePanel; diff --git a/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.context.tsx b/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.context.tsx index 56a40c166..6a6d7c9b0 100644 --- a/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.context.tsx +++ b/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.context.tsx @@ -2,13 +2,16 @@ import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from import { FormProvider, useForm } from 'react-hook-form'; import omit from 'lodash/omit'; import useStep from '@/hooks/useStep'; +import { Step } from '@/core/models/Step'; import { FormValues } from './StepConfigDrawer.types'; type Props = { workflowId: string; stepIndex: number }; type State = { workflowId: string; stepIndex: number; -} & ReturnType; + isLoading: boolean; + data?: Step; +}; const initialState: State = { data: undefined, @@ -24,7 +27,7 @@ const StepConfigDrawerProvider = ({ children, workflowId, stepIndex }: PropsWith const value = useMemo(() => { if (!result) return initialState; - return { workflowId, stepIndex, ...result }; + return { workflowId, stepIndex, ...result } as State; }, [result, workflowId, stepIndex]); useEffect(() => { @@ -32,17 +35,18 @@ const StepConfigDrawerProvider = ({ children, workflowId, stepIndex }: PropsWith return; } + const step = result.data as Step; form.reset({ configuration: { - is_always_run: result?.data?.mergedValues?.is_always_run ?? false, - is_skippable: result?.data?.mergedValues?.is_skippable ?? false, - run_if: result?.data?.mergedValues?.run_if ?? '', + is_always_run: step?.mergedValues?.is_always_run ?? false, + is_skippable: step?.mergedValues?.is_skippable ?? false, + run_if: step?.mergedValues?.run_if ?? '', }, properties: { - name: result?.data?.resolvedInfo?.title ?? '', - version: result?.data?.resolvedInfo?.normalizedVersion ?? '', + name: step?.title ?? '', + version: step?.resolvedInfo?.normalizedVersion ?? '', }, - inputs: result.data?.mergedValues?.inputs?.reduce((acc, input) => { + inputs: step?.mergedValues?.inputs?.reduce((acc, input) => { return { ...acc, ...omit(input, 'opts') }; }, {}), }); diff --git a/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.tsx b/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.tsx index d705e8ede..31d2ce456 100644 --- a/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.tsx +++ b/source/javascripts/components/unified-editor/StepConfigDrawer/StepConfigDrawer.tsx @@ -66,7 +66,7 @@ const StepConfigDrawerContent = (props: UseDisclosureProps) => { ({ properties: { name, version }, configuration: { is_always_run, is_skippable, run_if }, inputs }) => { updateStep(workflowId, stepIndex, { title: name, is_always_run, is_skippable, run_if }, defaultValues || {}); - const newInputs = Object.entries(inputs).map(([key, value]) => ({ + const newInputs = Object.entries(inputs || {}).map(([key, value]) => ({ [`${key}`]: value, })); updateStepInputs(workflowId, stepIndex, newInputs, defaultValues?.inputs || []); @@ -129,7 +129,7 @@ const StepConfigDrawerContent = (props: UseDisclosureProps) => { { - const stepId = step?.resolvedInfo?.id || ''; + const stepId = step?.id || ''; const isDisabled = Boolean(enabledSteps && !enabledSteps.has(stepId)); return { id: stepId, step, isDisabled }; }); diff --git a/source/javascripts/components/unified-editor/StepSelectorDrawer/components/VirtualizedRow.tsx b/source/javascripts/components/unified-editor/StepSelectorDrawer/components/VirtualizedRow.tsx index 401d0acb3..c2be21d8c 100644 --- a/source/javascripts/components/unified-editor/StepSelectorDrawer/components/VirtualizedRow.tsx +++ b/source/javascripts/components/unified-editor/StepSelectorDrawer/components/VirtualizedRow.tsx @@ -35,8 +35,8 @@ const VirtualizedRow = ({ item, style = {}, onSelectStep }: VirtualizedRowProps) {item.steps.map(({ id, step, isDisabled }) => ( { @@ -11,8 +12,8 @@ const useSearchSteps = ({ search, categories }: SearchFormValues) => { const index = useMemo(() => { const options = { keys: [ - { name: 'resolvedInfo.id', weight: 2 }, - { name: 'resolvedInfo.title', weight: 3 }, + { name: 'id', weight: 2 }, + { name: 'title', weight: 3 }, { name: 'defaultValues.summary', weight: 0.5 }, { name: 'defaultValues.type_tags' }, ], @@ -30,15 +31,15 @@ const useSearchSteps = ({ search, categories }: SearchFormValues) => { // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey: ['steps', { search, categories }], queryFn: async () => { - let items = steps || ([] as Step[]); + let items = steps || ([] as StepApiResult[]); const expressions = []; if (search) { const term = search.trim().toLowerCase(); const exp = { $or: [ - { $path: 'resolvedInfo.id', $val: term }, - { $path: 'resolvedInfo.title', $val: term }, + { $path: 'id', $val: term }, + { $path: 'title', $val: term }, { $path: 'defaultValues.summary', $val: term }, ], }; @@ -57,7 +58,7 @@ const useSearchSteps = ({ search, categories }: SearchFormValues) => { } if (expressions.length > 0) { - const results = index.search({ + const results = index.search({ $and: expressions, }); items = results.map((result) => result.item); diff --git a/source/javascripts/components/unified-editor/StepSelectorDrawer/hooks/useVirtualizedItems.ts b/source/javascripts/components/unified-editor/StepSelectorDrawer/hooks/useVirtualizedItems.ts index 5a86382ce..f3d1c870a 100644 --- a/source/javascripts/components/unified-editor/StepSelectorDrawer/hooks/useVirtualizedItems.ts +++ b/source/javascripts/components/unified-editor/StepSelectorDrawer/hooks/useVirtualizedItems.ts @@ -28,7 +28,7 @@ const useVirtualizedItems = ({ const virtualItems: VirtualizedListItem[] = []; if (highlightedStepGroups && Object.keys(highlightedStepGroups).length > 0) { Object.entries(highlightedStepGroups).forEach(([category, stepIds]) => { - const groupSteps = steps.filter((step) => stepIds.has(step?.resolvedInfo?.id || '')); + const groupSteps = steps.filter((step) => stepIds.has(step?.id || '')); virtualItems.push( ...createVirtualItemsGroup({ columns, diff --git a/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.stories.tsx b/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.stories.tsx deleted file mode 100644 index 2003b2a95..000000000 --- a/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import WithBlockPanel from './WithBlockPanel'; - -export default { - args: { - stepDisplayName: 'With group', - withBlockData: { - image: 'ruby:3.2', - services: ['postgres:13'], - }, - }, - component: WithBlockPanel, -} as Meta; - -export const WithBlock: StoryObj = {}; diff --git a/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.tsx b/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.tsx deleted file mode 100644 index 5e9f490c1..000000000 --- a/source/javascripts/components/unified-editor/WithBlockPanel/WithBlockPanel.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Input, Notification, TagsInput, Text } from '@bitrise/bitkit'; -import useNavigation from '@/hooks/useNavigation'; - -type WithBlockPanelProps = { - groupName: string; - imageName: string; - services: string[]; -}; - -const WithBlockPanel = ({ groupName, imageName, services }: WithBlockPanelProps) => { - const { replace } = useNavigation(); - - return ( - - - - {groupName} - - - - {!!imageName && ( - ref?.setAttribute('data-1p-ignore', '')} - marginBlockEnd="24" - value={imageName} - /> - )} - {services && services.length > 0 && ( - {}} - onRemove={() => {}} - /> - )} - replace('/yml'), - }} - status="info" - > - Edit container or service configuration - - View more details or edit the container or service configuration on the Configuration YAML page. - - - - - ); -}; - -export default WithBlockPanel; diff --git a/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.stories.tsx b/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.stories.tsx new file mode 100644 index 000000000..11797633b --- /dev/null +++ b/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { withBitriseYml } from '@/contexts/BitriseYmlProvider'; +import { MockYml } from '@/core/models/BitriseYml.mocks'; +import WithGroupDrawer from './WithGroupDrawer'; + +export default { + title: 'javascripts/components/unified-editor/WithGroup', + component: WithGroupDrawer, + args: { + isOpen: true, + workflowId: 'with-group', + stepIndex: 0, + }, + decorators: [(Story) => withBitriseYml(MockYml, Story)], +} as Meta; + +export const Drawer: StoryObj = {}; diff --git a/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.tsx b/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.tsx new file mode 100644 index 000000000..a0971023e --- /dev/null +++ b/source/javascripts/components/unified-editor/WithGroup/WithGroupDrawer.tsx @@ -0,0 +1,66 @@ +import { Icon, useDisclosure } from '@bitrise/bitkit'; +import { + Drawer, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + UseDisclosureProps, +} from '@chakra-ui/react'; +import useStep from '@/hooks/useStep'; +import StepService from '@/core/models/StepService'; +import { WithGroup } from '@/core/models/Step'; +import { WithBlockContent, WithBlockHeader } from './WithGroupPanel'; + +type Props = UseDisclosureProps & { + workflowId: string; + stepIndex: number; +}; + +const WithGroupDrawer = ({ workflowId, stepIndex, ...disclosureProps }: Props) => { + const { isOpen, onClose } = useDisclosure(disclosureProps); + const { data } = useStep(workflowId, stepIndex); + const isWithGroup = StepService.isWithGroup(data?.cvs || '', data?.userValues); + + if (!isWithGroup || !data) { + return null; + } + + const { + title, + userValues: { container = '', services = [] }, + } = data as WithGroup; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default WithGroupDrawer; diff --git a/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.stories.tsx b/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.stories.tsx new file mode 100644 index 000000000..b153d58a8 --- /dev/null +++ b/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from '@storybook/react'; +import WithGroupPanel from './WithGroupPanel'; + +export default { + title: 'javascripts/components/unified-editor/WithGroup', + component: WithGroupPanel, + args: { + groupName: 'With group', + imageName: 'ruby:3.2', + services: ['postgres', 'redis'], + }, +} as Meta; + +export const Panel: StoryObj = {}; diff --git a/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.tsx b/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.tsx new file mode 100644 index 000000000..ef30ce775 --- /dev/null +++ b/source/javascripts/components/unified-editor/WithGroup/WithGroupPanel.tsx @@ -0,0 +1,71 @@ +import { Box, Input, Notification, TagsInput, Text } from '@bitrise/bitkit'; +import useNavigation from '@/hooks/useNavigation'; + +type WithBlockPanelProps = { + groupName: string; + imageName: string; + services: string[]; +}; + +const WithBlockHeader = ({ title }: { title: string }) => ( + + + {title} + + +); + +const WithBlockContent = ({ imageName, services }: { imageName: string; services: string[] }) => { + const { replace } = useNavigation(); + + return ( + + {!!imageName && ( + ref?.setAttribute('data-1p-ignore', '')} + marginBlockEnd="24" + value={imageName} + /> + )} + {services && services.length > 0 && ( + {}} + onRemove={() => {}} + /> + )} + replace('/yml'), + }} + status="info" + > + Edit container or service configuration + + View more details or edit the container or service configuration on the Configuration YAML page. + + + + ); +}; + +const WithGroupPanel = ({ groupName, imageName, services }: WithBlockPanelProps) => { + return ( + + + + + ); +}; + +export { WithBlockPanelProps, WithBlockHeader, WithBlockContent }; + +export default WithGroupPanel; diff --git a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx index 3b4b52c9f..ffa443b90 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.tsx @@ -4,37 +4,21 @@ import useWorkflow from '@/hooks/useWorkflow'; import StackAndMachineService from '@/core/models/StackAndMachineService'; import WorkflowEmptyState from '../WorkflowEmptyState'; import useStacksAndMachines from '../WorkflowConfig/hooks/useStacksAndMachines'; +import { StepActions, WorkflowActions } from './WorkflowCard.types'; import StepList from './components/StepList'; import ChainedWorkflowList from './components/ChainedWorkflowList'; -import { WorkflowCardCallbacks } from './WorkflowCard.types'; import SortableWorkflowsContext from './components/SortableWorkflowsContext'; -type Props = WorkflowCardCallbacks & { +type Props = { id: string; isCollapsable?: boolean; containerProps?: CardProps; + workflowActions?: WorkflowActions; + stepActions?: StepActions; }; -const WorkflowCard = ({ id, isCollapsable, containerProps, ...callbacks }: Props) => { - const { - onCreateWorkflow, - onAddChainedWorkflowClick, - onAddStepClick, - onStepMove, - onStepSelect, - onUpgradeStep, - onCloneStep, - onDeleteStep, - } = callbacks; - const stepCallbacks = { - onAddStepClick, - onStepMove, - onStepSelect, - onUpgradeStep, - onCloneStep, - onDeleteStep, - }; - +const WorkflowCard = ({ id, isCollapsable, containerProps, workflowActions = {}, stepActions = {} }: Props) => { + const { onCreateWorkflow, onAddChainedWorkflowClick } = workflowActions; const workflow = useWorkflow(id); const containerRef = useRef(null); const { data: stacksAndMachines } = useStacksAndMachines(); @@ -96,20 +80,20 @@ const WorkflowCard = ({ id, isCollapsable, containerProps, ...callbacks }: Props before_run`} - {...callbacks} placement="before_run" parentWorkflowId={id} - onAddChainedWorkflowClick={onAddChainedWorkflowClick} + workflowActions={workflowActions} + stepActions={stepActions} /> - + after_run`} - {...callbacks} placement="after_run" parentWorkflowId={id} - onAddChainedWorkflowClick={onAddChainedWorkflowClick} + workflowActions={workflowActions} + stepActions={stepActions} /> diff --git a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.types.ts b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.types.ts index 517301f4a..20191fb1b 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.types.ts +++ b/source/javascripts/components/unified-editor/WorkflowCard/WorkflowCard.types.ts @@ -1,15 +1,7 @@ import { ChainedWorkflowPlacement as Placement } from '@/core/models/Workflow'; import { BitriseYmlStoreState } from '@/core/stores/BitriseYmlStore'; -export type WorkflowCardCallbacks = { - // Step related actions - onAddStepClick?: (workflowId: string, stepIndex: number) => void; - onStepSelect?: (workflowId: string, stepIndex: number) => void; - onStepMove?: BitriseYmlStoreState['moveStep']; - onUpgradeStep?: BitriseYmlStoreState['changeStepVersion']; - onCloneStep?: BitriseYmlStoreState['cloneStep']; - onDeleteStep?: BitriseYmlStoreState['deleteStep']; - // Workflow related actions +export type WorkflowActions = { onCreateWorkflow?: () => void; onEditWorkflowClick?: (workflowId: string) => void; onAddChainedWorkflowClick?: (workflowId: string) => void; @@ -17,6 +9,16 @@ export type WorkflowCardCallbacks = { onDeleteChainedWorkflowClick?: BitriseYmlStoreState['deleteChainedWorkflow']; }; +export type StepVariant = 'with-group' | 'step-bundle' | 'step'; +export type StepActions = { + onAddStepClick?: (workflowId: string, stepIndex: number) => void; + onStepSelect?: (workflowId: string, stepIndex: number, variant: StepVariant) => void; + onStepMove?: BitriseYmlStoreState['moveStep']; + onUpgradeStep?: BitriseYmlStoreState['changeStepVersion']; + onCloneStep?: BitriseYmlStoreState['cloneStep']; + onDeleteStep?: BitriseYmlStoreState['deleteStep']; +}; + export type SortableWorkflowItem = { id: string; index: number; diff --git a/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowCard.tsx b/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowCard.tsx index 19be8fd85..219b2fbc7 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowCard.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowCard.tsx @@ -8,12 +8,12 @@ import useWorkflow from '@/hooks/useWorkflow'; import DragHandle from '@/components/DragHandle/DragHandle'; import WorkflowService from '@/core/models/WorkflowService'; import useDependantWorkflows from '@/hooks/useDependantWorkflows'; -import { SortableWorkflowItem, WorkflowCardCallbacks } from '../WorkflowCard.types'; +import { SortableWorkflowItem, StepActions, WorkflowActions } from '../WorkflowCard.types'; import ChainedWorkflowList from './ChainedWorkflowList'; import StepList from './StepList'; import SortableWorkflowsContext from './SortableWorkflowsContext'; -type Props = WorkflowCardCallbacks & { +type Props = { id: string; index: number; uniqueId: string; @@ -21,6 +21,8 @@ type Props = WorkflowCardCallbacks & { isDragging?: boolean; parentWorkflowId: string; containerProps?: CardProps; + workflowActions?: WorkflowActions; + stepActions?: StepActions; }; const ChainedWorkflowCard = ({ @@ -31,15 +33,11 @@ const ChainedWorkflowCard = ({ isDragging, parentWorkflowId, containerProps, - ...callbacks + workflowActions = {}, + stepActions = {}, }: Props) => { - const { - onEditWorkflowClick, - onChainedWorkflowsUpdate, - onAddChainedWorkflowClick, - onDeleteChainedWorkflowClick, - ...stepCallbacks - } = callbacks; + const { onEditWorkflowClick, onChainedWorkflowsUpdate, onAddChainedWorkflowClick, onDeleteChainedWorkflowClick } = + workflowActions; const isEditable = Boolean(onEditWorkflowClick || onAddChainedWorkflowClick || onDeleteChainedWorkflowClick); const isSortable = Boolean(onChainedWorkflowsUpdate); @@ -121,6 +119,7 @@ const ChainedWorkflowCard = ({ tabIndex={-1} // NOTE: Without this, the tooltip always appears when closing any drawers on the Workflows page. className="nopan" onClick={onToggle} + isDisabled={isDragging} iconName={isOpen ? 'ChevronUp' : 'ChevronDown'} aria-label={`${isOpen ? 'Collapse' : 'Expand'} workflow details`} tooltipProps={{ 'aria-label': `${isOpen ? 'Collapse' : 'Expand'} workflow details` }} @@ -178,14 +177,21 @@ const ChainedWorkflowCard = ({ before_run`} - {...callbacks} placement="before_run" parentWorkflowId={id} + workflowActions={workflowActions} + stepActions={stepActions} /> - + - after_run`} {...callbacks} placement="after_run" parentWorkflowId={id} /> + after_run`} + placement="after_run" + parentWorkflowId={id} + workflowActions={workflowActions} + stepActions={stepActions} + /> diff --git a/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowList.tsx b/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowList.tsx index eef9c2d4f..0d4168c89 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowList.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/components/ChainedWorkflowList.tsx @@ -7,14 +7,16 @@ import { useShallow } from 'zustand/react/shallow'; import { ChainedWorkflowPlacement as Placement } from '@/core/models/Workflow'; import useBitriseYmlStore from '@/hooks/useBitriseYmlStore'; import { useWorkflows } from '@/hooks/useWorkflows'; -import { SortableWorkflowItem, WorkflowCardCallbacks } from '../WorkflowCard.types'; +import { SortableWorkflowItem, StepActions, WorkflowActions } from '../WorkflowCard.types'; import ChainedWorkflowCard from './ChainedWorkflowCard'; import Droppable from './Droppable'; -type Props = WorkflowCardCallbacks & { +type Props = { placement: Placement; parentWorkflowId: string; containerProps?: BoxProps; + workflowActions?: WorkflowActions; + stepActions?: StepActions; }; function anotherPlacement(placement: Placement): Placement { @@ -41,8 +43,14 @@ function getSortableItemUniqueIds(sortableItems: SortableWorkflowItem[]) { return sortableItems.map((i) => i.uniqueId); } -const ChainedWorkflowList = ({ placement, containerProps, parentWorkflowId, ...callbacks }: Props) => { - const { onChainedWorkflowsUpdate } = callbacks; +const ChainedWorkflowList = ({ + placement, + containerProps, + parentWorkflowId, + workflowActions = {}, + stepActions = {}, +}: Props) => { + const { onChainedWorkflowsUpdate } = workflowActions; const isAfterRun = placement === 'after_run'; const isBeforeRun = placement === 'before_run'; const isSortable = Boolean(onChainedWorkflowsUpdate); @@ -189,7 +197,12 @@ const ChainedWorkflowList = ({ placement, containerProps, parentWorkflowId, ...c {isAfterRun && } {sortableItems.map((item) => ( - + ))} {isBeforeRun && } diff --git a/source/javascripts/components/unified-editor/WorkflowCard/components/SortableWorkflowsContext.tsx b/source/javascripts/components/unified-editor/WorkflowCard/components/SortableWorkflowsContext.tsx index 0aa11435e..42e27433a 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/components/SortableWorkflowsContext.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/components/SortableWorkflowsContext.tsx @@ -10,7 +10,6 @@ import { Modifier, } from '@dnd-kit/core'; import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers'; -import noop from 'lodash/noop'; import { SortableWorkflowItem } from '../WorkflowCard.types'; import ChainedWorkflowCard from './ChainedWorkflowCard'; @@ -62,9 +61,7 @@ const SortableWorkflowsContext = ({ children, containerRef }: Props) => { modifiers={[restrictToVerticalAxis, restrictToContainer]} > {children} - - {activeItem && } - + {activeItem && } ); }; diff --git a/source/javascripts/components/unified-editor/WorkflowCard/components/StepCard.tsx b/source/javascripts/components/unified-editor/WorkflowCard/components/StepCard.tsx index 261a3871a..e54102bfb 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/components/StepCard.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/components/StepCard.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Avatar, Box, ButtonGroup, Card, ControlButton, Skeleton, SkeletonBox, Text } from '@bitrise/bitkit'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -5,16 +6,18 @@ import useStep from '@/hooks/useStep'; import defaultIcon from '@/../images/step/icon-default.svg'; import DragHandle from '@/components/DragHandle/DragHandle'; import VersionUtils from '@/core/utils/VersionUtils'; -import { SortableStepItem, WorkflowCardCallbacks } from '../WorkflowCard.types'; +import { Step } from '@/core/models/Step'; +import StepService from '@/core/models/StepService'; +import { SortableStepItem, StepActions } from '../WorkflowCard.types'; -type StepCardProps = Pick & { +type StepCardProps = { uniqueId: string; - stepIndex: number; workflowId: string; + stepIndex: number; isSortable?: boolean; isDragging?: boolean; showSecondary?: boolean; - onClick?: WorkflowCardCallbacks['onStepSelect']; + actions?: StepActions; }; const StepCard = ({ @@ -24,12 +27,10 @@ const StepCard = ({ isSortable, isDragging, showSecondary = true, - onClick, - onUpgradeStep, - onCloneStep, - onDeleteStep, + actions = {}, }: StepCardProps) => { const result = useStep(workflowId, stepIndex); + const { onStepSelect, onUpgradeStep, onCloneStep, onDeleteStep } = actions; const sortable = useSortable({ id: uniqueId, @@ -41,14 +42,23 @@ const StepCard = ({ } satisfies SortableStepItem, }); - const isButton = Boolean(onClick); + const stepVariant = useMemo(() => { + if (StepService.isWithGroup(result?.data?.cvs || '')) { + return 'with-group'; + } + if (StepService.isStepBundle(result?.data?.cvs || '')) { + return 'step-bundle'; + } + return 'step'; + }, [result?.data?.cvs]); if (!result) { return null; } const { data, isLoading } = result; - const { cvs, resolvedInfo } = data ?? {}; + const { cvs, title, icon } = data ?? {}; + const resolvedInfo = (data as Step)?.resolvedInfo; const isUpgradable = onUpgradeStep && VersionUtils.hasVersionUpgrade(resolvedInfo?.normalizedVersion, resolvedInfo?.versions); const latestMajor = VersionUtils.latestMajor(resolvedInfo?.versions)?.toString() ?? ''; @@ -60,8 +70,8 @@ const StepCard = ({ - - {showSecondary && } + + {showSecondary && } @@ -90,7 +100,8 @@ const StepCard = ({ ); } - const handleClick = isButton ? () => onClick?.(workflowId, stepIndex) : undefined; + const isButton = Boolean(onStepSelect); + const handleClick = isButton ? () => onStepSelect?.(workflowId, stepIndex, stepVariant) : undefined; return ( - {resolvedInfo?.title || cvs} + {title || cvs || ''} {showSecondary && ( diff --git a/source/javascripts/components/unified-editor/WorkflowCard/components/StepList.tsx b/source/javascripts/components/unified-editor/WorkflowCard/components/StepList.tsx index 1e3db5359..22b357a2b 100644 --- a/source/javascripts/components/unified-editor/WorkflowCard/components/StepList.tsx +++ b/source/javascripts/components/unified-editor/WorkflowCard/components/StepList.tsx @@ -5,29 +5,27 @@ import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-ki import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { useShallow } from 'zustand/react/shallow'; import useBitriseYmlStore from '@/hooks/useBitriseYmlStore'; -import { SortableStepItem, WorkflowCardCallbacks } from '../WorkflowCard.types'; +import { SortableStepItem, StepActions } from '../WorkflowCard.types'; import AddStepButton from './AddStepButton'; import StepCard from './StepCard'; -type Props = Pick< - WorkflowCardCallbacks, - 'onAddStepClick' | 'onStepMove' | 'onStepSelect' | 'onUpgradeStep' | 'onCloneStep' | 'onDeleteStep' -> & { +type Props = { workflowId: string; containerProps?: BoxProps; + stepActions?: StepActions; }; function getSortableItemUniqueIds(sortableItems: SortableStepItem[]) { return sortableItems.map((i) => i.uniqueId); } -const StepList = ({ workflowId, containerProps, ...callbacks }: Props) => { +const StepList = ({ workflowId, containerProps, stepActions }: Props) => { const steps = useBitriseYmlStore( useShallow(({ yml }) => { return (yml.workflows?.[workflowId]?.steps ?? []).map((s) => JSON.stringify(s)); }), ); - const { onAddStepClick, onStepMove, onStepSelect, ...actions } = callbacks; + const { onAddStepClick, onStepMove, ...actions } = stepActions ?? {}; const initialSortableItems: SortableStepItem[] = useMemo(() => { return steps.map((_, stepIndex) => ({ @@ -95,7 +93,7 @@ const StepList = ({ workflowId, containerProps, ...callbacks }: Props) => { return ( {sortableItems.map((item) => { - return ; + return ; })} ); @@ -124,7 +122,7 @@ const StepList = ({ workflowId, containerProps, ...callbacks }: Props) => { zIndex={10} onClick={onAddStepClick && (() => onAddStepClick(workflowId, item.stepIndex))} /> - + {isLast && ( ('*/api/step-info', async ({ request }) => { + const requestData = await request.json(); + await delay(); + + switch (status) { + case 'success': + return HttpResponse.json( + { + id: requestData.id, + title: requestData.id, + inputs: [{ company: 'Bitrise' }], + }, + { status: 200 }, + ); + + case 'error': + default: + return new HttpResponse(null, { + status: 404, + statusText: 'Not Found', + }); + } + }); +} + +export default { getAlgoliaSteps, getLocalStep }; diff --git a/source/javascripts/core/api/StepApi.ts b/source/javascripts/core/api/StepApi.ts index 6d829d801..355e323b3 100644 --- a/source/javascripts/core/api/StepApi.ts +++ b/source/javascripts/core/api/StepApi.ts @@ -11,7 +11,7 @@ const ALGOLIA_API_KEY = '708f890e859e7c44f309a1bbad3d2de8'; const ALGOLIA_STEPLIB_STEPS_INDEX = 'steplib_steps'; const ALGOLIA_STEPLIB_INPUTS_INDEX = 'steplib_inputs'; -// DTOs +// TYPES type AlgoliaStepResponse = { readonly objectID: string; id: string; @@ -41,19 +41,17 @@ type AlgoliaStepInputResponse = { [key: string]: unknown; }; +type StepApiResult = Required> | undefined; + // TRANSFORMATIONS -function toStep( - cvs: string, - response: Partial, - versions: string[] = [], -): Required> | undefined { - const { version = '' } = StepService.parseStepCVS(cvs); +function toStep(cvs: string, response: Partial, versions: string[] = []): StepApiResult { + const { id, version = '' } = StepService.parseStepCVS(cvs); if (!response.id) { return undefined; } const title = StepService.resolveTitle(response.cvs || cvs, response.step); - const icon = StepService.resolveIcon(response.step, response.info); + const icon = StepService.resolveIcon(cvs, response.step, response.info); const normalizedVersion = VersionUtils.normalizeVersion(version); const resolvedVersion = response.version || VersionUtils.resolveVersion(version, versions); const latestVersion = response.latest_version_number || VersionUtils.resolveVersion('', versions); @@ -65,11 +63,11 @@ function toStep( return { cvs: response.cvs || cvs, + id: response.id || id, + title, + icon, defaultValues: response.step ?? {}, resolvedInfo: { - id: response.id, - title, - icon, versions, version, normalizedVersion, @@ -104,7 +102,7 @@ function getAlgoliaClients() { }; } -async function getAlgoliaSteps(): Promise { +async function getAlgoliaSteps(): Promise { const { stepsClient } = getAlgoliaClients(); const results: Array = []; await stepsClient.browseObjects({ @@ -113,10 +111,10 @@ async function getAlgoliaSteps(): Promise { }); return uniqBy(results, 'id') .map((step) => toStep(step.cvs, step)) - .filter(Boolean) as Step[]; + .filter(Boolean) as StepApiResult[]; } -async function getStepByCvs(cvs: string): Promise { +async function getStepByCvs(cvs: string): Promise { if (StepService.isStepLibStep(cvs)) { if (StepService.isCustomStepLibStep(cvs)) { return getCustomStepByCvs(cvs); @@ -136,7 +134,7 @@ async function getStepByCvs(cvs: string): Promise { return undefined; } -async function getAlgoliaStepByCvs(cvs: string): Promise { +async function getAlgoliaStepByCvs(cvs: string): Promise { const { id, version } = StepService.parseStepCVS(cvs); const { stepsClient } = getAlgoliaClients(); const results: AlgoliaStepResponse[] = []; @@ -156,13 +154,13 @@ async function getAlgoliaStepByCvs(cvs: string): Promise { } return result; }) - .filter(Boolean) as Step[]; + .filter(Boolean) as StepApiResult[]; return steps?.find( - ({ resolvedInfo }) => resolvedInfo?.resolvedVersion === VersionUtils.resolveVersion(version, availableVersions), + (step) => step?.resolvedInfo?.resolvedVersion === VersionUtils.resolveVersion(version, availableVersions), ); } -async function getCustomStepByCvs(cvs: string): Promise { +async function getCustomStepByCvs(cvs: string): Promise { if (!/https?:\/\//.test(cvs)) { return undefined; } @@ -173,7 +171,7 @@ async function getCustomStepByCvs(cvs: string): Promise { return toStep(cvs, { id, version }); } -async function getDirectGitStepByCvs(cvs: string): Promise { +async function getDirectGitStepByCvs(cvs: string): Promise { if (!StepService.isGitStep(cvs)) { return undefined; } @@ -184,7 +182,7 @@ async function getDirectGitStepByCvs(cvs: string): Promise { return toStep(cvs, { id, version }); } -async function getLocalStepByCvs(cvs: string): Promise { +async function getLocalStepByCvs(cvs: string): Promise { if (!StepService.isLocalStep(cvs)) { return undefined; } @@ -211,7 +209,7 @@ async function getAlgoliaStepInputsByCvs(cvs: string): Promise & { image?: string }; -type StepLike = StepYmlObject | StepBundleYmlObject | WithGroupYmlObject; +type StepLikeYmlObject = StepYmlObject | StepBundleYmlObject | WithGroupYmlObject; + type Step = { cvs: string; + id: string; + title: string; + icon: string; defaultValues?: StepYmlObject; // The defaults are coming from the step.yml file loaded from the API - userValues?: StepYmlObject; // The values are coming from the bitrise.yml file defined by the user - mergedValues?: StepYmlObject; // the merged values of the defaults and user values - resolvedInfo?: ResolvedStepInfo; + userValues: StepYmlObject; // The values are coming from the bitrise.yml file defined by the user + mergedValues: StepYmlObject; // the merged values of the defaults and user values + resolvedInfo: ResolvedStepInfo; }; -type ResolvedStepInfo = Partial<{ +type WithGroup = { + cvs: string; + id: string; + title: string; + icon: string; + userValues: WithGroupYmlObject; +}; + +type StepBundle = { + cvs: string; id: string; title: string; icon: string; + userValues: StepBundleYmlObject; +}; + +type StepLike = Step | WithGroup | StepBundle; + +type ResolvedStepInfo = Partial<{ version: string; // 2 || 2.1 || 2.1.6 normalizedVersion: string; // 2.x.x resolvedVersion: string; // 2.1.6 @@ -82,11 +101,14 @@ type StepOutputVariable = StepVariable; export { Steps, - StepLike, + StepLikeYmlObject, StepYmlObject, StepBundleYmlObject, WithGroupYmlObject, Step, + StepLike, + WithGroup, + StepBundle, ResolvedStepInfo, StepVariable, StepInputVariable, diff --git a/source/javascripts/core/models/StepService.spec.ts b/source/javascripts/core/models/StepService.spec.ts index 861708cbb..3d9fafe6f 100644 --- a/source/javascripts/core/models/StepService.spec.ts +++ b/source/javascripts/core/models/StepService.spec.ts @@ -1,3 +1,4 @@ +import { StepApiResult } from '@/core/api/StepApi'; import StepService from './StepService'; import { Step } from './Step'; @@ -416,26 +417,26 @@ describe('StepService', () => { describe('resolveIcon', () => { it('should return step icon.svg if available', () => { const step = { asset_urls: { 'icon.svg': 'step-icon.svg' } }; - expect(StepService.resolveIcon(step)).toBe('step-icon.svg'); + expect(StepService.resolveIcon('script', step)).toBe('step-icon.svg'); }); it('should return step icon.png if icon.svg is not available', () => { const step = { asset_urls: { 'icon.png': 'step-icon.png' } }; - expect(StepService.resolveIcon(step)).toBe('step-icon.png'); + expect(StepService.resolveIcon('script', step)).toBe('step-icon.png'); }); it('should return info icon.svg if step icon is not available', () => { const info = { asset_urls: { 'icon.svg': 'info-icon.svg' } }; - expect(StepService.resolveIcon(undefined, info)).toBe('info-icon.svg'); + expect(StepService.resolveIcon('script', undefined, info)).toBe('info-icon.svg'); }); it('should return info icon.png if step and info icon.svg are not available', () => { const info = { asset_urls: { 'icon.png': 'info-icon.png' } }; - expect(StepService.resolveIcon(undefined, info)).toBe('info-icon.png'); + expect(StepService.resolveIcon('script', undefined, info)).toBe('info-icon.png'); }); it('should return default icon if no icons are available', () => { - expect(StepService.resolveIcon()).toBe('default-icon'); + expect(StepService.resolveIcon('script')).toBe('default-icon'); }); }); @@ -444,14 +445,11 @@ describe('StepService', () => { const step = { cvs: 'script@1', resolvedInfo: { - cvs: 'script@1', - id: 'script', version: '1', - icon: '', normalizedVersion: '1.x.x', versions: ['1.0.0', '1.1.0', '2.0.0', '2.2.2'], }, - }; + } as Step; expect(StepService.getSelectableVersions(step)).toStrictEqual([ { value: '', label: 'Always latest' }, { value: '2.2.x', label: '2.2.x - Patch updates only' }, @@ -467,14 +465,11 @@ describe('StepService', () => { const step = { cvs: 'script@1', resolvedInfo: { - cvs: 'script@1', - id: 'script', version: '1.1.0', - icon: '', normalizedVersion: '1.1.0', versions: ['1.0.0', '1.1.0', '2.0.0', '2.2.2'], }, - }; + } as Step; expect(StepService.getSelectableVersions(step)).toStrictEqual([ { value: '', label: 'Always latest' }, { value: '1.1.0', label: '1.1.0 - Version in bitrise.yml' }, @@ -491,14 +486,11 @@ describe('StepService', () => { const step = { cvs: 'script', resolvedInfo: { - cvs: 'script', - id: 'script', version: '', - icon: '', normalizedVersion: '', - versions: [], + versions: [] as string[], }, - }; + } as Step; expect(StepService.getSelectableVersions(step)).toEqual([{ value: '', label: 'Always latest' }]); }); }); @@ -523,7 +515,7 @@ describe('StepService', () => { describe('getInputNames', () => { it('should return an empty array if step has no inputs', () => { - const step = { defaultValues: {} } as Step; + const step = { defaultValues: {} } as StepApiResult; const result = StepService.getInputNames(step); expect(result).toEqual([]); }); @@ -533,7 +525,7 @@ describe('StepService', () => { defaultValues: { inputs: [{ input1: 'value1', opts: {} }, { input2: 'value2' }, { opts: {} }, { input3: 'value3' }], }, - } as Step; + } as StepApiResult; const result = StepService.getInputNames(step); expect(result).toEqual(['input1', 'input2', 'input3']); }); @@ -545,12 +537,12 @@ describe('StepService', () => { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.0.0' }, defaultValues: { inputs: [{ input1: 'value1' }, { input2: 'value2' }] }, - } as Step; + } as unknown as StepApiResult; const newStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: '2.0.0' }, defaultValues: { inputs: [{ input2: 'value2' }, { input3: 'value3' }] }, - } as Step; + } as unknown as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: ['input1'], newInputs: ['input3'], @@ -563,14 +555,14 @@ describe('StepService', () => { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.0.0' }, defaultValues: { inputs: [{ input1: 'value1' }, { input2: 'value2' }] }, - } as Step; + } as unknown as StepApiResult; const newStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.2.0' }, defaultValues: { inputs: [{ input1: 'value1' }, { input2: 'value2' }, { input3: 'value3' }], }, - } as Step; + } as unknown as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: [], newInputs: ['input3'], @@ -579,12 +571,12 @@ describe('StepService', () => { }); it('returns no changes if oldStep or newStep is missing', () => { - expect(StepService.calculateChange(undefined, {} as Step)).toEqual({ + expect(StepService.calculateChange(undefined, {} as StepApiResult)).toEqual({ removedInputs: [], newInputs: [], change: 'none', }); - expect(StepService.calculateChange({} as Step, undefined)).toEqual({ + expect(StepService.calculateChange({} as StepApiResult, undefined)).toEqual({ removedInputs: [], newInputs: [], change: 'none', @@ -592,8 +584,8 @@ describe('StepService', () => { }); it('returns no changes if step IDs are different', () => { - const oldStep = { cvs: 'script@1' } as Step; - const newStep = { cvs: 'other-script@1' } as Step; + const oldStep = { cvs: 'script@1' } as StepApiResult; + const newStep = { cvs: 'other-script@1' } as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: [], newInputs: [], @@ -605,11 +597,11 @@ describe('StepService', () => { const oldStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.0.0' }, - } as Step; + } as StepApiResult; const newStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.0.0' }, - } as Step; + } as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: [], newInputs: [], @@ -622,12 +614,12 @@ describe('StepService', () => { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.0.0' }, defaultValues: { inputs: [{ input1: 'value1' }, { input2: 'value2' }] }, - } as Step; + } as unknown as StepApiResult; const newStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: '1.2.0' }, defaultValues: { inputs: [{ input1: 'value1' }, { input2: 'value2' }] }, - } as Step; + } as unknown as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: [], newInputs: [], @@ -639,11 +631,11 @@ describe('StepService', () => { const oldStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: 'master' }, - } as Step; + } as StepApiResult; const newStep = { cvs: 'script@1', resolvedInfo: { resolvedVersion: 'master' }, - } as Step; + } as StepApiResult; expect(StepService.calculateChange(oldStep, newStep)).toEqual({ removedInputs: [], newInputs: [], diff --git a/source/javascripts/core/models/StepService.ts b/source/javascripts/core/models/StepService.ts index 413e8c337..c20a63e40 100644 --- a/source/javascripts/core/models/StepService.ts +++ b/source/javascripts/core/models/StepService.ts @@ -8,12 +8,13 @@ import { Step, StepBundleYmlObject, StepInputVariable, + StepLikeYmlObject, Steps, StepYmlObject, VariableOpts, WithGroupYmlObject, } from '@/core/models/Step'; -import type { StepInfo } from '@/core/api/StepApi'; +import type { StepApiResult, StepInfo } from '@/core/api/StepApi'; import VersionUtils from '@/core/utils/VersionUtils'; import defaultIcon from '@/../images/step/icon-default.svg'; @@ -103,7 +104,7 @@ function isWithGroup(cvs: string, _step?: Steps[number][string]): _step is WithG return /^with$/g.test(cvs); } -function resolveTitle(cvs: string, step?: StepYmlObject): string { +function resolveTitle(cvs: string, step?: Steps[number][string]): string { if (isStepBundle(cvs, step)) { return `Step bundle: ${cvs.replace('bundle::', '')}`; } @@ -119,7 +120,11 @@ function resolveTitle(cvs: string, step?: StepYmlObject): string { return id.split('/').pop() || id; } -function resolveIcon(step?: StepYmlObject, info?: StepInfo): string { +function resolveIcon(cvs: string, step?: StepLikeYmlObject, info?: StepInfo): string { + if (isWithGroup(cvs, step) || isStepBundle(cvs, step)) { + return defaultIcon; + } + return ( step?.asset_urls?.['icon.svg'] || step?.asset_urls?.['icon.png'] || @@ -152,11 +157,11 @@ function getSelectableVersions(step?: Step): Array<{ value: string; label: strin return results; } -function getStepCategories(steps: Step[]): string[] { - return uniq(steps.flatMap((step) => step.defaultValues?.type_tags || [])).sort(); +function getStepCategories(steps: StepApiResult[] | Step[]): string[] { + return uniq(steps.flatMap((step) => step?.defaultValues?.type_tags || [])).sort(); } -function getInputNames(step?: Step): string[] { +function getInputNames(step?: StepApiResult): string[] { if (!step?.defaultValues?.inputs) { return []; } @@ -165,8 +170,8 @@ function getInputNames(step?: Step): string[] { } function calculateChange( - oldStep: Step | undefined, - newStep: Step | undefined, + oldStep: StepApiResult | undefined, + newStep: StepApiResult | undefined, ): { newInputs: string[]; removedInputs: string[]; diff --git a/source/javascripts/hooks/useEnvVars.ts b/source/javascripts/hooks/useEnvVars.ts index b293e871d..3c05f68fb 100644 --- a/source/javascripts/hooks/useEnvVars.ts +++ b/source/javascripts/hooks/useEnvVars.ts @@ -85,7 +85,7 @@ const useStepLevelEnvVars = (workflowId: string, enabled: boolean) => { if (!isLoading) { result.forEach(({ data: step }) => { - const source = step?.resolvedInfo?.title || step?.resolvedInfo?.id || step?.cvs || ''; + const source = step?.title || step?.id || step?.cvs || ''; step?.defaultValues?.outputs?.forEach((ymlEnvVar) => { const env = EnvVarService.parseYmlEnvVar(ymlEnvVar, source); envVarMap.set(env.key, env); diff --git a/source/javascripts/hooks/useStep.ts b/source/javascripts/hooks/useStep.ts index e904ec57b..ebca6e9e8 100644 --- a/source/javascripts/hooks/useStep.ts +++ b/source/javascripts/hooks/useStep.ts @@ -3,12 +3,12 @@ import { useShallow } from 'zustand/react/shallow'; import { useQuery } from '@tanstack/react-query'; import merge from 'lodash/merge'; import useBitriseYmlStore from '@/hooks/useBitriseYmlStore'; -import { Step, StepYmlObject } from '@/core/models/Step'; +import { Step, StepBundle, StepLike, WithGroup } from '@/core/models/Step'; import StepService from '@/core/models/StepService'; -import StepApi from '@/core/api/StepApi'; +import StepApi, { StepApiResult } from '@/core/api/StepApi'; type YmlStepResult = { - data?: Required>; + data?: StepLike; }; function useStepFromYml(workflowId: string, stepIndex: number): YmlStepResult { @@ -26,106 +26,134 @@ function useStepFromYml(workflowId: string, stepIndex: number): YmlStepResult { return { data: undefined }; } - // TODO handle step bundle and with group - if (StepService.isStepBundle(cvs, step) || StepService.isWithGroup(cvs, step)) { - return { data: { cvs, userValues: step as StepYmlObject } }; + const { id } = StepService.parseStepCVS(cvs); + const title = StepService.resolveTitle(cvs, step); + const icon = StepService.resolveIcon(cvs, step); + + if (StepService.isWithGroup(cvs, step)) { + return { data: { cvs, id, title, icon, userValues: step } }; + } + if (StepService.isStepBundle(cvs, step)) { + return { data: { cvs, id, title, icon, userValues: step } }; + } + if (StepService.isStep(cvs, step)) { + return { + data: { + cvs, + id, + title: step.title || '', // step.title is optional, but might got a default value from the API + icon: step.asset_urls?.['icon.svg'] || step.asset_urls?.['icon.png'] || '', // step.asset_urls is optional, but might got a default value from the API + defaultValues: {}, + userValues: step, + mergedValues: step, + resolvedInfo: {}, + }, + }; } - return { - data: { - cvs, - userValues: step as StepYmlObject, - }, - }; + return { data: undefined }; }), ); } type ApiStepResult = { - data?: Required>; + data?: StepApiResult; isLoading: boolean; }; function useStepFromApi(cvs = ''): ApiStepResult { - const { data, isLoading: isLoadingStep } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ['steps', { cvs }], queryFn: () => StepApi.getStepByCvs(cvs), - enabled: Boolean(cvs), + enabled: Boolean(cvs && !StepService.isStepBundle(cvs) && !StepService.isWithGroup(cvs)), staleTime: Infinity, }); return useMemo(() => { + if (!cvs) { + return { data: undefined, isLoading: false }; + } + + if (!data) { + return { data: undefined, isLoading }; + } + return { data: { cvs, + id: data.id, + title: data.title, + icon: data.icon, defaultValues: { ...(data?.defaultValues ?? {}), inputs: data?.defaultValues?.inputs || [], }, resolvedInfo: data?.resolvedInfo ?? {}, }, - isLoading: isLoadingStep, + isLoading, }; - }, [cvs, data, isLoadingStep]); + }, [cvs, data, isLoading]); } type UseStepResult = { - data?: Step; + data?: Step | WithGroup | StepBundle; isLoading?: boolean; }; const useStep = (workflowId: string, stepIndex: number): UseStepResult => { const { data: ymlData } = useStepFromYml(workflowId, stepIndex); - const cvs = ymlData?.cvs ?? ''; - - const { data: apiData, isLoading: isLoadingApi } = useStepFromApi(cvs); + const { data: apiData, isLoading } = useStepFromApi(ymlData?.cvs ?? ''); return useMemo(() => { - const userValues = ymlData?.userValues ?? {}; - const defaultValues = apiData?.defaultValues ?? {}; - const resolvedInfo = apiData?.resolvedInfo ?? {}; + const { cvs, id, title, icon, userValues } = ymlData ?? {}; + const { title: defaultTitle, icon: defaultIcon, defaultValues, resolvedInfo } = apiData ?? {}; - if (!cvs) { + if (!cvs || !id) { return { data: undefined, isLoading: false }; } - if (StepService.isStepBundle(cvs) || StepService.isWithGroup(cvs)) { + if (StepService.isWithGroup(cvs, userValues)) { return { - data: { - cvs, - defaultValues: {}, - userValues, - mergedValues: {}, - resolvedInfo: {}, - }, - isLoading: isLoadingApi, + data: ymlData as WithGroup, + isLoading: false, + }; + } + + if (StepService.isStepBundle(cvs, userValues)) { + return { + data: ymlData as StepBundle, + isLoading: false, }; } - const inputs = defaultValues?.inputs?.map(({ opts, ...input }) => { - const [inputName, defaultValue] = Object.entries(input)[0]; - const inputFromYml = userValues?.inputs?.find(({ opts: _, ...inputObjectFromYml }) => { - const inputNameFromYml = Object.keys(inputObjectFromYml)[0]; - return inputNameFromYml === inputName; + if (StepService.isStep(cvs, userValues)) { + const inputs = defaultValues?.inputs?.map(({ opts, ...input }) => { + const [inputName, defaultValue] = Object.entries(input)[0]; + const inputFromYml = userValues?.inputs?.find(({ opts: _, ...inputObjectFromYml }) => { + const inputNameFromYml = Object.keys(inputObjectFromYml)[0]; + return inputNameFromYml === inputName; + }); + + return { opts, [inputName]: inputFromYml?.[inputName] ?? defaultValue }; }); - return { opts, [inputName]: inputFromYml?.[inputName] ?? defaultValue }; - }); + return { + data: { + cvs, + id, + title: title || defaultTitle || '', + icon: icon || defaultIcon || '', + defaultValues, + userValues, + mergedValues: merge({}, defaultValues, userValues, { inputs }), + resolvedInfo, + } as Step, + isLoading, + }; + } - return { - data: { - cvs, - defaultValues, - userValues, - mergedValues: merge({}, defaultValues, userValues, { inputs }), - resolvedInfo: { - ...resolvedInfo, - title: userValues?.title || resolvedInfo?.title || cvs, - }, - }, - isLoading: isLoadingApi, - }; - }, [cvs, ymlData, apiData, isLoadingApi]); + return { data: undefined, isLoading: false }; + }, [ymlData, apiData, isLoading]); }; export default useStep; diff --git a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.store.ts b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.store.ts index 3e75c28d0..b37c51a10 100644 --- a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.store.ts +++ b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.store.ts @@ -10,7 +10,9 @@ type State = { | 'delete-workflow' | 'step-config-drawer' | 'step-selector-drawer' - | 'workflow-config-drawer'; + | 'workflow-config-drawer' + | 'with-group-drawer' + | 'step-bundle-drawer'; }; type Action = { @@ -22,6 +24,8 @@ type Action = { openWorkflowConfigDrawer: (workflowId: string) => void; openStepConfigDrawer: (workflowId: string, stepIndex: number) => void; openStepSelectorDrawer: (workflowId: string, stepIndex: number) => void; + openWithGroupConfigDrawer: (workflowId: string, stepIndex: number) => void; + openStepBundleDrawer: (workflowId: string, stepIndex: number) => void; }; export const useWorkflowsPageStore = create((set) => ({ @@ -64,4 +68,18 @@ export const useWorkflowsPageStore = create((set) => ({ isDialogOpen: 'step-selector-drawer', })); }, + openWithGroupConfigDrawer: (workflowId, stepIndex) => { + return set(() => ({ + workflowId, + stepIndex, + isDialogOpen: 'with-group-drawer', + })); + }, + openStepBundleDrawer: (workflowId, stepIndex) => { + return set(() => ({ + workflowId, + stepIndex, + isDialogOpen: 'step-bundle-drawer', + })); + }, })); diff --git a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.stories.tsx b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.stories.tsx index 4b1fbf145..1df8c63d7 100644 --- a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.stories.tsx +++ b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.stories.tsx @@ -3,11 +3,12 @@ import { Box } from '@bitrise/bitkit'; import { MockYml } from '@/core/models/BitriseYml.mocks'; import { getStacksAndMachines } from '@/core/api/StacksAndMachinesApi.mswMocks'; import { getSecretsFromApi, getSecretsFromLocal } from '@/core/api/SecretApi.mswMocks'; +import StepApiMocks from '@/core/api/StepApi.mswMocks'; import { getCertificates, - getProvProfiles, getDefaultOutputs, getFileStorageDocuments, + getProvProfiles, } from '@/core/api/EnvVarsApi.mswMocks'; import WorkflowsPage from './WorkflowsPage'; @@ -22,6 +23,7 @@ const meta: Meta = { layout: 'fullscreen', msw: { handlers: [ + StepApiMocks.getLocalStep({ status: 'success' }), getCertificates(), getProvProfiles(), getSecretsFromApi(), @@ -52,7 +54,12 @@ const cliStory: Story = { parameters: { layout: 'fullscreen', msw: { - handlers: [getSecretsFromLocal(), getDefaultOutputs(), getStacksAndMachines()], + handlers: [ + StepApiMocks.getLocalStep({ status: 'success' }), + getSecretsFromLocal(), + getDefaultOutputs(), + getStacksAndMachines(), + ], }, }, }; diff --git a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.tsx b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.tsx index a91df23f1..cac3d2496 100644 --- a/source/javascripts/pages/WorkflowsPage/WorkflowsPage.tsx +++ b/source/javascripts/pages/WorkflowsPage/WorkflowsPage.tsx @@ -6,17 +6,19 @@ import { ChainWorkflowDrawer, CreateWorkflowDialog, RunWorkflowDialog, + StepBundleDrawer, StepConfigDrawer, StepSelectorDrawer, + WithGroupDrawer, WorkflowConfigDrawer, WorkflowConfigPanel, WorkflowEmptyState, } from '@/components/unified-editor'; import useBitriseYmlStore from '@/hooks/useBitriseYmlStore'; import useSelectedWorkflow from '@/hooks/useSelectedWorkflow'; -import WorkflowCanvasPanel from './components.new/WorkflowCanvasPanel/WorkflowCanvasPanel'; import { useWorkflowsPageStore } from './WorkflowsPage.store'; import DeleteWorkflowDialog from './components.new/DeleteWorkflowDialog/DeleteWorkflowDialog'; +import WorkflowCanvasPanel from './components.new/WorkflowCanvasPanel/WorkflowCanvasPanel'; type Props = { yml: BitriseYml; @@ -40,18 +42,22 @@ const WorkflowsPageContent = () => { const { enabledSteps, - isStepConfigDrawerOpen, isRunWorkflowDialogOpen, + isStepConfigDrawerOpen, isStepSelectorDrawerOpen, + isWithBlockDrawerOpen, + isStepBundleDrawerOpen, isChainWorkflowDrawerOpen, isCreateWorkflowDialogOpen, isDeleteWorkflowDialogOpen, isWorkflowConfigDrawerOpen, } = { enabledSteps: new Set(getUniqueStepIds()), - isStepConfigDrawerOpen: isDialogOpen === 'step-config-drawer', isRunWorkflowDialogOpen: isDialogOpen === 'run-workflow', + isStepConfigDrawerOpen: isDialogOpen === 'step-config-drawer', isStepSelectorDrawerOpen: isDialogOpen === 'step-selector-drawer', + isWithBlockDrawerOpen: isDialogOpen === 'with-group-drawer', + isStepBundleDrawerOpen: isDialogOpen === 'step-bundle-drawer', isChainWorkflowDrawerOpen: isDialogOpen === 'chain-workflow', isCreateWorkflowDialogOpen: isDialogOpen === 'create-workflow', isDeleteWorkflowDialogOpen: isDialogOpen === 'delete-workflow', @@ -111,6 +117,20 @@ const WorkflowsPageContent = () => { onClose={closeDialog} /> + + + + { const { openStepConfigDrawer, + openWithGroupConfigDrawer, + openStepBundleDrawer, openStepSelectorDrawer, openRunWorkflowDialog, openChainWorkflowDialog, openWorkflowConfigDrawer, } = useWorkflowsPageStore(); + const openStepLikeDrawer: StepActions['onStepSelect'] = (wfId, stepIndex, variant) => { + switch (variant) { + case 'with-group': + openWithGroupConfigDrawer(wfId, stepIndex); + break; + case 'step-bundle': + openStepBundleDrawer(wfId, stepIndex); + break; + default: + openStepConfigDrawer(wfId, stepIndex); + break; + } + }; + return ( @@ -57,16 +74,20 @@ const WorkflowCanvasPanel = ({ workflowId }: Props) => { diff --git a/source/javascripts/pages/WorkflowsPage/index.ts b/source/javascripts/pages/WorkflowsPage/index.ts index 8d02f89ba..36f04cbf7 100644 --- a/source/javascripts/pages/WorkflowsPage/index.ts +++ b/source/javascripts/pages/WorkflowsPage/index.ts @@ -1,5 +1,5 @@ -import StepBundlePanel from '@/components/unified-editor/StepBundlePanel/StepBundlePanel'; -import WithBlockPanel from '@/components/unified-editor/WithBlockPanel/WithBlockPanel'; +import StepBundlePanel from '@/components/unified-editor/StepBundle/StepBundlePanel'; +import WithGroupPanel from '@/components/unified-editor/WithGroup/WithGroupPanel'; import WorkflowToolbar from './components/WorkflowMainToolbar'; import WorkflowConfigPanel from './components/WorkflowConfigPanel/WorkflowConfigPanel'; import ChainWorkflowDrawer from './components/ChainWorkflowDrawer/ChainWorkflowDrawer'; @@ -19,5 +19,5 @@ export { VersionChangeDialog, DeleteWorkflowDialog, StepBundlePanel, - WithBlockPanel, + WithGroupPanel, }; diff --git a/source/templates/workflows.slim b/source/templates/workflows.slim index 640e94404..252de4bc5 100644 --- a/source/templates/workflows.slim +++ b/source/templates/workflows.slim @@ -81,7 +81,7 @@ div[ng-if="workflowsCtrl.selectedStep.isStepBundle()"] r-step-bundle-panel[bundle-name="workflowsCtrl.selectedStep.displayName()"] div[ng-if="workflowsCtrl.selectedStep.isWithBlock()"] - r-with-block-panel[group-name="workflowsCtrl.selectedStep.displayName()" image-name="workflowsCtrl.selectedStep.withBlockData.image" services="workflowsCtrl.selectedStep.withBlockData.services"] + r-with-group-panel[group-name="workflowsCtrl.selectedStep.displayName()" image-name="workflowsCtrl.selectedStep.withBlockData.image" services="workflowsCtrl.selectedStep.withBlockData.services"] section.workflow[ng-if='!workflowsCtrl.selectedStep && (workflowsCtrl.editedWorkflow && workflowsCtrl.yml)'] r-workflow-config-panel[ yml="workflowsCtrl.yml"