Skip to content
Open
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
@@ -0,0 +1,120 @@
import React from 'react'
import { DACBotCheckboxComponent } from 'src/components/dac_bot/DACBotCheckboxComponent'
import { DAC } from 'src/libs/ajax/DAC'
import { ParsedDACbotRule } from 'src/components/dac_bot/DACBotComponent'

describe('DACBotCheckboxComponent', () => {
const mockRule = {
id: 1,
ruleType: 'REQUIRE_SO_DAR_APPROVAL',
description: 'Require Signing Official approval for all data access requests',
ruleState: 'AVAILABLE',
activationDate: 0,
enabledByUserId: null,
displayName: null,
userEmail: null,
exclusiveRuleType: 'AUTO_OPEN_DAR_FOR_ALL_MEMBERS',
isDisabled: false,
} as ParsedDACbotRule

const mockEnabledRule = {
...mockRule,
enabledByUserId: 123,
activationDate: Date.now(),
displayName: 'John Doe',
userEmail: '[email protected]',
}

beforeEach(() => {
cy.stub(DAC, 'toggleDACbotRule').resolves({
ruleId: 1,
isRuleEnabled: true,
enabledTime: Date.now(),
displayName: 'Test User',
email: '[email protected]',
})
})

it('should render checkbox with rule description', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={false}
/>,
)
cy.contains('Require Signing Official approval').should('be.visible')
})

it('should be disabled when disableEdit is true', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={true}
/>,
)
cy.get('[id="1_checkbox"]').should('be.disabled')
})

it('should be enabled when disableEdit is false', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={false}
/>,
)
cy.get('[id="1_checkbox"]').should('not.be.disabled')
})

it('should display enabled user info when rule is enabled', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockEnabledRule}
disableEdit={false}
/>,
)
cy.contains('Enabled by:').should('be.visible')
cy.contains('John Doe').should('be.visible')
cy.contains('a', 'John Doe').should('have.attr', 'href', 'mailto:[email protected]')
})

it('should not display enabled user info when rule is disabled', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={false}
/>,
)
cy.contains('Enabled by:').should('not.exist')
})

it('should call onRuleChange callback when checkbox is clicked', () => {
const onRuleChange = cy.stub()
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={false}
onRuleChange={onRuleChange}
/>,
)
cy.get('[id="1_checkbox"]').click()
cy.wrap(onRuleChange).should('have.been.called')
})

it('should show success notification on successful toggle', () => {
cy.mount(
<DACBotCheckboxComponent
dacId={1}
rule={mockRule}
disableEdit={false}
/>,
)
cy.get('[id="1_checkbox"]').click()
cy.contains('Automation rule successfully saved.').should('be.visible')
})
})
69 changes: 69 additions & 0 deletions cypress/component/components/dac_bot/DACBotComponent.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'
import { DACBotComponent } from 'src/components/dac_bot/DACBotComponent'
import { DAC } from 'src/libs/ajax/DAC'
import { Storage } from 'src/libs/storage'

describe('DACBotComponent', () => {
const mockRules = [
{
id: 1,
ruleType: 'REQUIRE_SO_DAR_APPROVAL',
description: 'Require SO approval for data access',
ruleState: 'AVAILABLE',
activationDate: 0,
enabledByUserId: 1,
displayName: 'Test User',
userEmail: null,
},
{
id: 2,
ruleType: 'AUTO_OPEN_DAR_FOR_ALL_MEMBERS',
description: 'Auto open DAR for all members',
ruleState: 'AVAILABLE',
activationDate: 0,
enabledByUserId: null,
displayName: null,
userEmail: null,
},
]

beforeEach(() => {
// Mock the user to be a chair so checkboxes are enabled
cy.stub(Storage, 'getCurrentUser').returns({
roles: [{ dacId: 1, name: 'Chairperson' }],
})
cy.stub(DAC, 'fetchDACbotRules').resolves(mockRules)
cy.stub(DAC, 'toggleDACbotRule').resolves({
ruleId: 1,
isRuleEnabled: true,
enabledTime: Date.now(),
displayName: 'Test User',
email: '[email protected]',
})
cy.mount(<DACBotComponent dacId={1} data-cy="dac-bot-component" />)
})

it('should render component with heading and description', () => {
cy.contains('h4', 'Rule Automated Data Access Request (RADAR) Settings').should('be.visible')
cy.contains('p', 'Data Access Committees may automate Data Access Requests').should('be.visible')
})

it('should display all rules from API', () => {
cy.get('[id="1_checkbox"]').should('exist')
cy.get('[id="2_checkbox"]').should('exist')
})

it('should disable exclusive rule checkbox when other is enabled', () => {
cy.get('[id="2_checkbox"]').should('be.disabled')
})

it('should call toggleDACbotRule API when checkbox is clicked', () => {
cy.get('[id="1_checkbox"]').click()
cy.wrap(DAC.toggleDACbotRule).should('have.been.called')
})

it('should display enabled rule info with user details', () => {
cy.contains('Enabled by:').should('be.visible')
cy.contains('Test User').should('be.visible')
})
})
44 changes: 29 additions & 15 deletions src/components/dac_bot/DACBotCheckboxComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useState } from 'react'
import { DACbotRule } from './DACBotComponent'
import { FormField, FormFieldTypes } from '../forms/forms'
import { DAC } from '../../libs/ajax/DAC'
import { DACbotRule, ParsedDACbotRule } from 'src/components/dac_bot/DACBotComponent'
import { FormField, FormFieldTypes } from 'src/components/forms/forms'
import { DAC } from 'src/libs/ajax/DAC'
import { Link } from '@mui/material'
import { Notifications } from '../../libs/utils'
import { Notifications } from 'src/libs/utils'
import ReactMarkdown from 'react-markdown'

export type DACBotCheckboxComponentProps = {
dacId: number
rule: DACbotRule
rule: DACbotRule | ParsedDACbotRule
disableEdit: boolean
onRuleChange?: (rule: ParsedDACbotRule, isEnabled: boolean) => Promise<void>
}

export type DACBotToggleResult = {
Expand All @@ -21,7 +22,7 @@ export type DACBotToggleResult = {
}

export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) => {
const { dacId, rule, disableEdit } = props
const { dacId, rule, disableEdit, onRuleChange } = props
const [isReadOnly, setIsReadOnly] = useState(disableEdit)
const [isRuleEnabled, setIsRuleEnabled] = useState(!!rule.enabledByUserId)
const [enabledTime, setEnabledTime] = useState(rule.activationDate)
Expand All @@ -31,7 +32,21 @@ export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) =>
const onCheckboxChange = async () => {
setIsReadOnly(true)
try {
const toggleResult: DACBotToggleResult = await DAC.toggleDACbotRule(dacId, rule.id)
const newEnabledState = !isRuleEnabled

// If a custom onRuleChange callback is provided, use it
if (onRuleChange) {
await onRuleChange(rule as ParsedDACbotRule, newEnabledState)
}
else {
// Fallback to direct toggle
const toggleResult: DACBotToggleResult = await DAC.toggleDACbotRule(dacId, rule.id)
setIsRuleEnabled(toggleResult.isRuleEnabled)
setEnabledTime(toggleResult.enabledTime)
setDisplayName(toggleResult.displayName)
setEmailAddress(toggleResult.email)
}

Notifications.showSuccess(
{
severity: 'success',
Expand All @@ -41,14 +56,12 @@ export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) =>
vertical: 'bottom',
horizontal: 'right',
},
})
},
)
setIsReadOnly(false)
setIsRuleEnabled(toggleResult.isRuleEnabled)
setEnabledTime(toggleResult.enabledTime)
setDisplayName(toggleResult.displayName)
setEmailAddress(toggleResult.email)
}
catch (_) {
setIsReadOnly(false)
Notifications.showError(
{
severity: 'error',
Expand All @@ -58,7 +71,8 @@ export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) =>
vertical: 'bottom',
horizontal: 'right',
},
})
},
)
}
}

Expand All @@ -70,7 +84,7 @@ export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) =>
<>
<span style={{ display: 'table' }}>
<ReactMarkdown components={{
// Map `p` to use `span`s to align with the checkbox.
// Map `p` to use `span` to align with the checkbox.
p: 'span',
}}
>
Expand All @@ -93,7 +107,7 @@ export const DACBotCheckboxComponent = (props: DACBotCheckboxComponentProps) =>
: ``}
</>
)}
defaultValue={rule.enabledByUserId != null}
defaultValue={!!rule.enabledByUserId}
onChange={onCheckboxChange}
disabled={isReadOnly}
/>
Expand Down
68 changes: 60 additions & 8 deletions src/components/dac_bot/DACBotComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { DAC } from 'src/libs/ajax/DAC'
import { Notifications } from 'src/libs/utils'
import { DACBotCheckboxComponent } from './DACBotCheckboxComponent'
Expand Down Expand Up @@ -27,9 +27,14 @@ export type DACbotRule = {
userEmail: string | null
}

export type DACbotChangeResult = {
ruleId: number
isRuleEnabled: boolean
export type ParsedDACbotRule = DACbotRule & {
exclusiveRuleType?: string
isDisabled: boolean
}

const MUTUALLY_EXCLUSIVE_RULES: { [key: string]: string } = {
REQUIRE_SO_DAR_APPROVAL: 'AUTO_OPEN_DAR_FOR_ALL_MEMBERS',
AUTO_OPEN_DAR_FOR_ALL_MEMBERS: 'REQUIRE_SO_DAR_APPROVAL',
}

export const DACBotComponent = (props: DACBotComponentProps) => {
Expand All @@ -38,6 +43,19 @@ export const DACBotComponent = (props: DACBotComponentProps) => {
const [isLoading, setIsLoading] = useState(true)
const userIsChair = Storage.getCurrentUser().roles.some((r: UserRole) => r.dacId == dacId && r.name == 'Chairperson')

const parsedRules = useMemo(() => {
return DACbotRules.map((rule: DACbotRule) => {
const exclusiveRuleType = MUTUALLY_EXCLUSIVE_RULES[rule.ruleType]
const isExclusiveRuleEnabled = exclusiveRuleType && DACbotRules.some((r: DACbotRule) => r.ruleType === exclusiveRuleType && r.enabledByUserId)

return {
...rule,
exclusiveRuleType,
isDisabled: !!isExclusiveRuleEnabled,
}
})
}, [DACbotRules])

const fetchData = useCallback(async () => {
try {
setIsLoading(true)
Expand All @@ -60,9 +78,43 @@ export const DACBotComponent = (props: DACBotComponentProps) => {
}
}, [dacId])

const handleRuleChange = useCallback(async (rule: ParsedDACbotRule, isEnabled: boolean) => {
try {
// Toggle the current rule
await DAC.toggleDACbotRule(dacId, rule.id)

// If enabling this rule, disable its exclusive counterpart
if (isEnabled && rule.exclusiveRuleType) {
const exclusiveRule = DACbotRules.find(r => r.ruleType === rule.exclusiveRuleType)
if (exclusiveRule?.enabledByUserId) {
await DAC.toggleDACbotRule(dacId, exclusiveRule.id)
}
}

// Refresh rules to get updated state
await fetchData()
}
catch (_error) {
Notifications.showError(
{
severity: 'error',
text: 'Error: Unable to update automation rule. Please try again.',
timeout: 3500,
layout: {
vertical: 'bottom',
horizontal: 'right',
},
},
)
console.error('Failed to fetch DAC bot rules:', _error)
}
}, [dacId, DACbotRules, fetchData])

useEffect(() => {
fetchData().then()
}, [dacId, fetchData, setDACbotRules, setIsLoading])
(async () => {
await fetchData()
})()
}, [dacId, fetchData])

return (
<div data-cy={dataCy} data-dac-id={dacId.toString()}>
Expand All @@ -89,8 +141,8 @@ export const DACBotComponent = (props: DACBotComponentProps) => {
Check the box below to opt in to this feature, and then select the data use terms for which Data Access Requests you would like automated.
</p>
<h5>Rules</h5>
{!isLoading && DACbotRules.map((rule) => {
return <DACBotCheckboxComponent dacId={dacId} rule={rule} key={rule.id} disableEdit={!userIsChair} />
{!isLoading && parsedRules.map((rule) => {
return <DACBotCheckboxComponent dacId={dacId} rule={rule} key={rule.id} disableEdit={!userIsChair || rule.isDisabled} onRuleChange={handleRuleChange} />
})}
</div>
)
Expand Down
Loading