From 25a67d1bd9eaad48b08fb9de7c14bc1d2bc43938 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 13:02:05 -0700 Subject: [PATCH 1/2] fix(connectors): explain disabled settings with tooltips --- .../connector-access-field.test.tsx | 12 +++++++++++- .../connector-access-field.tsx | 19 ++++++++++++++++++- .../connector-settings-fields.tsx | 7 ------- .../edit-connector-modal.tsx | 1 + .../use-connector-settings-form.ts | 1 - 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx index 61664330801..9c48db78d18 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx @@ -123,7 +123,17 @@ describe('connection method selection', () => { expect(container.textContent).toContain(label) expect(container.textContent).not.toContain('Add a new connection') expect(container.querySelector('[role="radiogroup"]')).toBeNull() - expect(container.querySelector('button')).toBeNull() + const trigger = container.querySelector('button')! + expect(trigger).toBeDisabled() + expect(document.querySelector('[role="tooltip"]')).toBeNull() + await act(async () => { + trigger.parentElement!.dispatchEvent( + new MouseEvent('pointerover', { bubbles: true, clientX: 200, clientY: 200 }) + ) + }) + expect(document.querySelector('[role="tooltip"]')).toHaveTextContent( + 'Add a new connection to change the sync method.' + ) expect(onChange).not.toHaveBeenCalled() }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx index 12f32dab613..3725d89e07e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx @@ -5,9 +5,11 @@ import { ChipButtonGroup, ChipButtonGroupItem, ChipCombobox, + ChipDropdown, ChipLink, ChipModalField, type ComboboxOption, + Tooltip, } from '@sim/emcn' import type { ConnectorAccessMode } from '@/lib/api/contracts/knowledge/connectors' import { type ResourceScope, resourceScopeFromOwner } from '@/lib/core/resource-scope' @@ -164,7 +166,22 @@ export function ConnectorAccessField({ } >
- {slackSetupOnly ? null : !lockAccessMode && showModeSelector ? ( + {slackSetupOnly ? null : lockAccessMode ? ( + + + + ({ value: mode, label }))} + disabled + className='pointer-events-none w-fit' + /> + + + Add a new connection to change the sync method. + + ) : showModeSelector ? ( void canAdmin: boolean @@ -134,7 +133,6 @@ export function ConnectorSettingsFields({ hasMaxAccess, isSaving, error, - saveBlockedReason, access, onAccessChange, canAdmin, @@ -553,11 +551,6 @@ export function ConnectorSettingsFields({ )} - {saveBlockedReason && ( -

- {saveBlockedReason} -

- )} {error} ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx index 298ba33260d..4c6b9471546 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx @@ -108,6 +108,7 @@ export function EditConnectorModal({ label: form.saving ? 'Saving…' : 'Save', onClick: form.save, disabled: !form.canSave, + disabledTooltip: form.saveBlockedReason, }} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts index 53dec40f0a5..ada38059c68 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts @@ -519,7 +519,6 @@ export function useConnectorSettingsForm({ hasMaxAccess, isSaving, error: error ?? searchSetupError, - saveBlockedReason, access, onAccessChange: setAccess, canAdmin, From 1a69cec3500c90564479474a5b2c66042a5619cb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 18 Sep 2026 13:15:04 -0700 Subject: [PATCH 2/2] fix(settings): preserve accessible disabled-action explanations --- .../integrations/search-source-setup.test.tsx | 3 +- .../connector-access-field.test.tsx | 9 +++++ .../connector-access-field.tsx | 2 +- .../knowledge/[id]/components/consts.ts | 2 +- .../connector-settings-fields.test.tsx | 8 +++++ .../connector-settings-fields.tsx | 7 +++- .../use-connector-settings-form.ts | 1 + .../settings/settings-header-shell.test.tsx | 26 ++++++++++++++ .../components/settings/settings-header.tsx | 2 +- .../components/chip-modal/chip-modal.test.tsx | 34 +++++++++++++++++++ .../src/components/chip-modal/chip-modal.tsx | 4 +-- 11 files changed, 91 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/search-source-setup.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/search-source-setup.test.tsx index 8b191c6ae24..07ec5c2c42d 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/integrations/search-source-setup.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/search-source-setup.test.tsx @@ -988,7 +988,8 @@ describe('member content credentials in real add and edit dialogs', () => { await chooseSyncFrequency('Manual only') expect(document.body.textContent).toContain('Documents become unavailable after 24 hours') await chooseSyncFrequency('Every hour') - expect(document.body.textContent).toContain('Permissions are checked on every sync.') + expect(document.body.textContent).not.toContain('Documents become unavailable after 24 hours') + expect(document.body.textContent).not.toContain('Permissions are checked on every sync.') }) it('saves source settings without changing a dedicated indexing account', async () => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx index 9c48db78d18..f642de988cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.test.tsx @@ -134,6 +134,15 @@ describe('connection method selection', () => { expect(document.querySelector('[role="tooltip"]')).toHaveTextContent( 'Add a new connection to change the sync method.' ) + await act(async () => { + trigger.parentElement!.dispatchEvent(new MouseEvent('pointerout', { bubbles: true })) + }) + expect(document.querySelector('[role="tooltip"]')).toBeNull() + expect(trigger.parentElement!.tabIndex).toBe(0) + await act(async () => trigger.parentElement!.focus()) + expect(document.querySelector('[role="tooltip"]')).toHaveTextContent( + 'Add a new connection to change the sync method.' + ) expect(onChange).not.toHaveBeenCalled() }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx index 3725d89e07e..bd2cc556aa0 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx @@ -168,7 +168,7 @@ export function ConnectorAccessField({
{slackSetupOnly ? null : lockAccessMode ? ( - + { }) } + it('announces blocked saves without showing persistent helper text', async () => { + const reason = 'Wait for the current sync to finish before saving.' + await render(confluenceConnectorMeta, { saveBlockedReason: reason }) + const status = container.querySelector('[role="status"]') + expect(status).toHaveClass('sr-only') + expect(status).toHaveTextContent(reason) + }) + async function openAccountChoices() { const dropdown = container.querySelector('[role="combobox"]') if (!dropdown) throw new Error('Missing indexing-account selector') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx index ed786e352f9..0b696fb0044 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx @@ -91,6 +91,7 @@ export interface ConnectorSettingsFieldsProps { hasMaxAccess: boolean isSaving: boolean error: string | null + saveBlockedReason?: string access: ConnectorAccessSelection onAccessChange: (access: ConnectorAccessSelection) => void canAdmin: boolean @@ -133,6 +134,7 @@ export function ConnectorSettingsFields({ hasMaxAccess, isSaving, error, + saveBlockedReason, access, onAccessChange, canAdmin, @@ -463,7 +465,7 @@ export function ConnectorSettingsFields({ title='Account for browsing' hint={ isSearchIndex - ? 'Used to browse available content. Each person connects separately from Integrations to sync their Search content.' + ? 'Members sync with their own accounts connected in Integrations.' : undefined } > @@ -551,6 +553,9 @@ export function ConnectorSettingsFields({ )} +

+ {saveBlockedReason} +

{error} ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts index ada38059c68..53dec40f0a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts @@ -519,6 +519,7 @@ export function useConnectorSettingsForm({ hasMaxAccess, isSaving, error: error ?? searchSetupError, + saveBlockedReason, access, onAccessChange: setAccess, canAdmin, diff --git a/apps/sim/components/settings/settings-header-shell.test.tsx b/apps/sim/components/settings/settings-header-shell.test.tsx index ecf53c2706a..cabc42b06f1 100644 --- a/apps/sim/components/settings/settings-header-shell.test.tsx +++ b/apps/sim/components/settings/settings-header-shell.test.tsx @@ -68,6 +68,32 @@ function clickChip(label: string) { } describe('SettingsHeaderShell action routing', () => { + it('makes disabled Save explanations keyboard reachable without enabling Save', () => { + const onSave = vi.fn() + const reason = 'Wait for the current sync to finish before saving.' + renderHeader( + saveDiscardActions({ + dirty: true, + saving: false, + saveDisabled: true, + saveTooltip: reason, + onSave, + onDiscard: vi.fn(), + }) + ) + const save = [...container.querySelectorAll('button')].find( + (button) => button.textContent === 'Save' + )! + const trigger = save.parentElement! + expect(save.disabled).toBe(true) + expect(trigger.tabIndex).toBe(0) + act(() => trigger.focus()) + expect(document.activeElement).toBe(trigger) + expect(document.querySelector('[role="tooltip"]')?.textContent).toBe(reason) + act(() => save.click()) + expect(onSave).not.toHaveBeenCalled() + }) + it('renders Delete before Discard and Save even though the array lists it last', () => { const actions: SettingsAction[] = [ ...saveDiscardActions({ dirty: true, saving: false, onSave: vi.fn(), onDiscard: vi.fn() }), diff --git a/apps/sim/components/settings/settings-header.tsx b/apps/sim/components/settings/settings-header.tsx index 27788ef7293..6fde21eb670 100644 --- a/apps/sim/components/settings/settings-header.tsx +++ b/apps/sim/components/settings/settings-header.tsx @@ -196,7 +196,7 @@ export function SettingsActionChip({ if (!action.tooltip) return chip return ( - + {chip} {action.tooltip} diff --git a/packages/emcn/src/components/chip-modal/chip-modal.test.tsx b/packages/emcn/src/components/chip-modal/chip-modal.test.tsx index c6305e37e32..53a6a1dd16b 100644 --- a/packages/emcn/src/components/chip-modal/chip-modal.test.tsx +++ b/packages/emcn/src/components/chip-modal/chip-modal.test.tsx @@ -341,6 +341,40 @@ describe('ChipModalField file actions', () => { describe('ChipModal default actions', () => { beforeEach(makeElementsVisible) + it.each(['save', 'confirm'] as const)( + 'makes a disabled %s explanation reachable without enabling the action', + (variant) => { + const onClick = vi.fn() + const action = { + label: 'Save', + disabled: true, + disabledTooltip: 'Wait for the current sync to finish before saving.', + onClick, + } + mount( + variant === 'confirm' ? ( + {}} title='Save settings' confirm={action} /> + ) : ( + {}} srTitle='Save settings'> + {}}>Save settings + {}} primaryAction={action} /> + + ) + ) + + const save = buttonByText('Save') + const trigger = save.parentElement! + expect(save.disabled).toBe(true) + expect(trigger.tabIndex).toBe(0) + act(() => trigger.focus()) + expect(document.activeElement).toBe(trigger) + expect(document.querySelector('[role="tooltip"]')?.textContent).toBe(action.disabledTooltip) + pressEnter(trigger) + act(() => save.click()) + expect(onClick).not.toHaveBeenCalled() + } + ) + it('fails safe to the dismiss decision in a confirmation', () => { mount( - + {primaryChip} {primaryAction.disabledTooltip} @@ -1649,7 +1649,7 @@ function renderChipConfirmButton( if (!confirm.disabledTooltip || !disabled) return chip return ( - + {chip} {confirm.disabledTooltip}