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 61664330801..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
@@ -123,7 +123,26 @@ 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.'
+ )
+ 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 12f32dab613..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
@@ -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 ? (
{
})
}
+ 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 4cb850c6ce9..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
@@ -465,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
}
>
@@ -553,11 +553,9 @@ export function ConnectorSettingsFields({
)}
- {saveBlockedReason && (
-
- {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/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}