From 663ece708e5b6c18aa3978cd3d283a716869ca28 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Fri, 13 Mar 2026 11:29:04 +0530 Subject: [PATCH] Feat/openapi sync beta tag (#7461) * feat: introduce OpenAPI Sync beta feedback feature - Added a feedback section in the OpenAPISyncTab and ConnectSpecForm to encourage user input during the beta phase. - Styled the feedback message and button for better visibility. - Updated the beta features list to include OpenAPI Sync and adjusted related components to reflect its beta status with appropriate badges. - Enhanced the StatusBadge component to support a new 'xs' size for better integration in various UI elements. * feat: integrate OpenAPI Sync beta feature toggle - Updated the ImportCollectionLocation component to conditionally enable the "Check for Spec Updates" option based on the OpenAPI Sync beta feature status. - Modified default preferences to disable OpenAPI Sync by default, ensuring users are not prompted for updates unless explicitly enabled. * feat: enhance beta features integration in Preferences - Updated the BETA_FEATURES array to use constants from utils/beta-features for better maintainability. - Improved the handling of beta preferences by merging new preferences with existing ones, ensuring a smoother user experience when toggling features. * feat: enhance OpenAPI Sync polling with beta feature toggle - Integrated a beta feature toggle for OpenAPI Sync polling, allowing conditional activation based on user settings. - Updated the pollingEnabled logic to incorporate the new beta feature status, ensuring better control over sync behavior. --- .../OpenAPISyncTab/ConnectSpecForm/index.js | 11 +++++++++ .../OpenAPISyncTab/StyledWrapper.js | 20 ++++++++++++++++ .../src/components/OpenAPISyncTab/index.js | 11 +++++++++ .../src/components/Preferences/Beta/index.js | 24 ++++++++++++------- .../RequestTabs/CollectionHeader/index.js | 12 ++++++---- .../RequestTabs/RequestTab/SpecialTab.js | 4 +++- .../Sidebar/Collections/Collection/index.js | 8 +++++-- .../Sidebar/ImportCollectionLocation/index.js | 6 +++-- .../providers/App/useOpenAPISyncPolling.js | 6 +++-- packages/bruno-app/src/utils/beta-features.js | 3 ++- .../bruno-electron/src/store/preferences.js | 5 +++- 11 files changed, 88 insertions(+), 22 deletions(-) diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/ConnectSpecForm/index.js b/packages/bruno-app/src/components/OpenAPISyncTab/ConnectSpecForm/index.js index 19e29d54d..5069272a5 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/ConnectSpecForm/index.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/ConnectSpecForm/index.js @@ -124,6 +124,17 @@ const ConnectSpecForm = ({ sourceUrl, setSourceUrl, isLoading, error, setError, ))} + +

+ OpenAPI Sync is in Beta — we'd love to hear your feedback and suggestions.{' '} + +

); }; diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js b/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js index 122a6bdc2..51ff3c57e 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js @@ -2278,6 +2278,26 @@ const StyledWrapper = styled.div` gap: 0.5rem; flex-shrink: 0; } + + .beta-feedback-inline { + margin-top: 2rem; + font-size: ${(props) => props.theme.font.size.xs}; + color: ${(props) => props.theme.colors.text.muted}; + + .beta-feedback-link { + background: none; + border: none; + padding: 0; + color: ${(props) => props.theme.status.info.text}; + cursor: pointer; + font-size: inherit; + text-decoration: underline; + + &:hover { + opacity: 0.8; + } + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/OpenAPISyncTab/index.js b/packages/bruno-app/src/components/OpenAPISyncTab/index.js index 58baae376..91335c23c 100644 --- a/packages/bruno-app/src/components/OpenAPISyncTab/index.js +++ b/packages/bruno-app/src/components/OpenAPISyncTab/index.js @@ -134,6 +134,16 @@ const OpenAPISyncTab = ({ collection }) => { fileNotFound={fileNotFound} onOpenSettings={() => setShowSettingsModal(true)} /> +

+ OpenAPI Sync is in Beta — we'd love to hear your feedback and suggestions.{' '} + +

)} @@ -195,6 +205,7 @@ const OpenAPISyncTab = ({ collection }) => { )} )} + {showSettingsModal && ( diff --git a/packages/bruno-app/src/components/Preferences/Beta/index.js b/packages/bruno-app/src/components/Preferences/Beta/index.js index 3da191500..cd1d68fb7 100644 --- a/packages/bruno-app/src/components/Preferences/Beta/index.js +++ b/packages/bruno-app/src/components/Preferences/Beta/index.js @@ -8,17 +8,19 @@ import debounce from 'lodash/debounce'; import toast from 'react-hot-toast'; import { IconFlask } from '@tabler/icons'; import get from 'lodash/get'; +import { BETA_FEATURES as BETA_FEATURE_IDS } from 'utils/beta-features'; /** - * Add beta features here. - * Example: - * { - * id: 'nodevm', - * label: 'Node VM Runtime', - * description: 'Enable Node VM runtime for JavaScript execution in Developer Mode' - * } + * UI metadata for beta features rendered in Preferences. + * IDs must match keys from utils/beta-features.js BETA_FEATURES. */ -const BETA_FEATURES = []; +const BETA_FEATURES = [ + { + id: BETA_FEATURE_IDS.OPENAPI_SYNC, + label: 'OpenAPI Sync', + description: 'Synchronize your Bruno collection with an OpenAPI specification. Detect drift, review changes, and sync with a single click.' + } +]; const Beta = ({ close }) => { const preferences = useSelector((state) => state.app.preferences); @@ -45,6 +47,7 @@ const Beta = ({ close }) => { const betaSchema = generateValidationSchema(); const formik = useFormik({ + enableReinitialize: true, initialValues: generateInitialValues(), validationSchema: betaSchema, onSubmit: async (values) => { @@ -61,7 +64,10 @@ const Beta = ({ close }) => { dispatch( savePreferences({ ...preferences, - beta: newBetaPreferences + beta: { + ...preferences.beta, + ...newBetaPreferences + } }) ) .catch((err) => console.log(err) && toast.error('Failed to update beta preferences')); diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js index dc93ae17d..693a30bbf 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js @@ -32,6 +32,8 @@ import { getRevealInFolderLabel } from 'utils/common/platform'; import classNames from 'classnames'; import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme'; +import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; +import StatusBadge from 'ui/StatusBadge/index'; const CollectionHeader = ({ collection, isScratchCollection }) => { const dispatch = useDispatch(); @@ -42,6 +44,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { // Get the current active workspace const currentWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); + const gitRootPath = collection?.git?.gitRootPath; + const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); // Workspace rename state const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false); @@ -201,8 +205,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { // Build overflow menu items for the "..." dropdown const overflowMenuItems = [ { id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables }, - ...(!hasOpenApiSyncConfigured - ? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, onClick: viewOpenApiSync }] + ...(isOpenAPISyncEnabled && !hasOpenApiSyncConfigured + ? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, rightSection: Beta, onClick: viewOpenApiSync }] : []), { id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings } ]; @@ -461,8 +465,8 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { {/* Right side: Actions (only for regular collections) */} {!isScratchCollection && (
- {/* OpenAPI Sync - standalone only when configured */} - {hasOpenApiSyncConfigured && ( + {/* OpenAPI Sync - standalone only when configured and beta enabled */} + {isOpenAPISyncEnabled && hasOpenApiSyncConfigured && ( { const getTabInfo = (type, tabName) => { @@ -90,7 +91,8 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra return ( <> - OpenAPI + OpenAPI + Beta ); } diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 3a58b05d3..fbe8dea27 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -48,6 +48,8 @@ import { getRevealInFolderLabel } from 'utils/common/platform'; import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal'; import ActionIcon from 'ui/ActionIcon'; import MenuDropdown from 'ui/MenuDropdown'; +import StatusBadge from 'ui/StatusBadge'; +import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext'; import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest'; @@ -56,6 +58,7 @@ import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest'; const EMPTY_STATE_DELAY_MS = 300; const Collection = ({ collection, searchText }) => { + const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); const { dropdownContainerRef } = useSidebarAccordion(); const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); @@ -382,12 +385,13 @@ const Collection = ({ collection, searchText }) => { setShowCloneCollectionModalOpen(true); } }, - { + ...(isOpenAPISyncEnabled ? [{ id: 'sync-openapi', leftSection: OpenAPISyncIcon, label: 'OpenAPI', + rightSection: Beta, onClick: openOpenAPISyncTab - }, + }] : []), ...(hasCopiedItems ? [ { diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index c0199d450..91bce94c9 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -13,6 +13,7 @@ import { processBrunoCollection } from 'utils/importers/bruno-collection'; import { processOpenCollection } from 'utils/importers/opencollection'; import { wsdlToBruno } from '@usebruno/converters'; import { toastError } from 'utils/common/error'; +import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; import Modal from 'components/Modal'; import Help from 'components/Help'; import Dropdown from 'components/Dropdown'; @@ -101,13 +102,14 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour const dispatch = useDispatch(); const [groupingType, setGroupingType] = useState('tags'); const [collectionFormat, setCollectionFormat] = useState(DEFAULT_COLLECTION_FORMAT); - const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(true); + const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); + const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(isOpenAPISyncEnabled); const dropdownTippyRef = useRef(); const isOpenApi = format === 'openapi'; const isZipImport = format === 'bruno-zip'; const isOpenApiFromUrl = isOpenApi && !!sourceUrl && !filePath; const isOpenApiFromFile = isOpenApi && !!filePath && !sourceUrl; - const showCheckForSpecUpdatesOption = isOpenApiFromUrl || isOpenApiFromFile; + const showCheckForSpecUpdatesOption = isOpenAPISyncEnabled && (isOpenApiFromUrl || isOpenApiFromFile); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const preferences = useSelector((state) => state.app.preferences); diff --git a/packages/bruno-app/src/providers/App/useOpenAPISyncPolling.js b/packages/bruno-app/src/providers/App/useOpenAPISyncPolling.js index aa5eb9027..21eba91f9 100644 --- a/packages/bruno-app/src/providers/App/useOpenAPISyncPolling.js +++ b/packages/bruno-app/src/providers/App/useOpenAPISyncPolling.js @@ -2,14 +2,16 @@ import { useEffect, useMemo, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { checkActiveWorkspaceCollectionsForUpdates } from 'providers/ReduxStore/slices/openapi-sync'; import { normalizePath } from 'utils/common/path'; +import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; const POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes const useOpenAPISyncPolling = () => { const dispatch = useDispatch(); - // Global toggle for pausing all OpenAPI sync polling (defaults to true, not yet wired to any UI) - const pollingEnabled = useSelector((state) => state.openapiSync?.pollingEnabled ?? true); + const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); + // Global toggle for pausing all OpenAPI sync polling + const pollingEnabled = useSelector((state) => state.openapiSync?.pollingEnabled ?? true) && isOpenAPISyncEnabled; const collections = useSelector((state) => state.collections?.collections || []); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); diff --git a/packages/bruno-app/src/utils/beta-features.js b/packages/bruno-app/src/utils/beta-features.js index 172e08499..84f9eb855 100644 --- a/packages/bruno-app/src/utils/beta-features.js +++ b/packages/bruno-app/src/utils/beta-features.js @@ -5,7 +5,8 @@ import { useSelector } from 'react-redux'; * Contains all available beta feature keys */ export const BETA_FEATURES = Object.freeze({ - NODE_VM: 'nodevm' + NODE_VM: 'nodevm', + OPENAPI_SYNC: 'openapi-sync' }); /** diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 48ed9db2a..ceb8ba5fe 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -45,7 +45,9 @@ const defaultPreferences = { layout: { responsePaneOrientation: 'horizontal' }, - beta: {}, + beta: { + 'openapi-sync': false + }, onboarding: { hasLaunchedBefore: false, hasSeenWelcomeModal: true @@ -154,6 +156,7 @@ const preferencesSchema = Yup.object().shape({ responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical']) }), beta: Yup.object({ + 'openapi-sync': Yup.boolean() }), onboarding: Yup.object({ hasLaunchedBefore: Yup.boolean(),