Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -164,7 +166,22 @@ export function ConnectorAccessField({
}
>
<div className='flex flex-col gap-2'>
{slackSetupOnly ? null : !lockAccessMode && showModeSelector ? (
{slackSetupOnly ? null : lockAccessMode ? (
<Tooltip.Root>
<Tooltip.Trigger asChild tabIndex={0}>
<span className='inline-flex w-fit cursor-not-allowed'>
<ChipDropdown
aria-label={`Sync using: ${currentMode?.label ?? 'Unavailable'}`}
value={value.accessMode}
options={visibleModes.map(({ mode, label }) => ({ value: mode, label }))}
disabled
className='pointer-events-none w-fit'
/>
</span>
</Tooltip.Trigger>
<Tooltip.Content>Add a new connection to change the sync method.</Tooltip.Content>
</Tooltip.Root>
) : showModeSelector ? (
<ChipButtonGroup
value={value.accessMode}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function connectorSyncFrequencyHint(
return 'Content and permissions update only when you sync. Documents become unavailable after 24 hours without a successful permission check.'
}
if (effectiveConnectorSyncIntervalMinutes(accessMode, syncInterval) === syncInterval) {
return 'Permissions are checked on every sync.'
return undefined
}
return accessMode === 'members' && hasContentCredential
? 'Content follows this schedule. Member permissions are checked every hour.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ describe('connector settings service-account choices', () => {
})
}

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<HTMLElement>('[role="combobox"]')
if (!dropdown) throw new Error('Missing indexing-account selector')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
>
Expand Down Expand Up @@ -553,11 +553,9 @@ export function ConnectorSettingsFields({
</ChipModalField>
)}

{saveBlockedReason && (
<p role='status' className='px-2 text-[var(--text-muted)] text-caption'>
Comment thread
waleedlatif1 marked this conversation as resolved.
{saveBlockedReason}
</p>
)}
<p role='status' className='sr-only'>
{saveBlockedReason}
</p>
<ChipModalError>{error}</ChipModalError>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export function EditConnectorModal({
label: form.saving ? 'Saving…' : 'Save',
onClick: form.save,
disabled: !form.canSave,
disabledTooltip: form.saveBlockedReason,
Comment thread
waleedlatif1 marked this conversation as resolved.
}}
/>
)}
Expand Down
26 changes: 26 additions & 0 deletions apps/sim/components/settings/settings-header-shell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() }),
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/components/settings/settings-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function SettingsActionChip({
if (!action.tooltip) return chip
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Tooltip.Trigger asChild tabIndex={action.disabled ? 0 : undefined}>
<span className='inline-flex'>{chip}</span>
</Tooltip.Trigger>
<Tooltip.Content>{action.tooltip}</Tooltip.Content>
Expand Down
34 changes: 34 additions & 0 deletions packages/emcn/src/components/chip-modal/chip-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' ? (
<ChipConfirmModal open onOpenChange={() => {}} title='Save settings' confirm={action} />
) : (
<ChipModal open onOpenChange={() => {}} srTitle='Save settings'>
<ChipModalHeader onClose={() => {}}>Save settings</ChipModalHeader>
<ChipModalFooter onCancel={() => {}} primaryAction={action} />
</ChipModal>
)
)

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(
<ChipConfirmModal
Expand Down
4 changes: 2 additions & 2 deletions packages/emcn/src/components/chip-modal/chip-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,7 @@ function ChipModalFooter({
{primaryAdjacentAction ? renderFooterSlotAction(primaryAdjacentAction) : null}
{showsDisabledTooltip && primaryAction ? (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Tooltip.Trigger asChild tabIndex={0}>
<span className='inline-flex cursor-not-allowed'>{primaryChip}</span>
</Tooltip.Trigger>
<Tooltip.Content>{primaryAction.disabledTooltip}</Tooltip.Content>
Expand Down Expand Up @@ -1649,7 +1649,7 @@ function renderChipConfirmButton(
if (!confirm.disabledTooltip || !disabled) return chip
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Tooltip.Trigger asChild tabIndex={0}>
<span className='inline-flex'>{chip}</span>
</Tooltip.Trigger>
<Tooltip.Content>{confirm.disabledTooltip}</Tooltip.Content>
Expand Down
Loading