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}