Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,8 @@ boxui.metadataInstanceEditor.editTooltip = Edit Metadata
boxui.metadataInstanceEditor.enableAIAutofill = Box AI Autofill
# Label for enable cascade policy toggle switch
boxui.metadataInstanceEditor.enableCascadePolicy = Enable Cascade Policy
# Name of the enhanced AI agent
boxui.metadataInstanceEditor.enhancedAgentName = Enhanced
# Message for users who may attempt to remove a custom metadata instance for a file. Also non-recoverable
boxui.metadataInstanceEditor.fileMetadataRemoveCustomTemplateConfirm = Are you sure you want to delete this custom metadata and all of its values from this file?
# Message for users who may attempt to remove a metadata instance for a file, which is non-recoverable
Expand All @@ -1288,6 +1290,8 @@ boxui.metadataInstanceEditor.noMetadataAddTemplate = Click 'Add' in the top righ
boxui.metadataInstanceEditor.operationNotImmediate = This operation is not immediate and may take some time.
# Label to remove a template
boxui.metadataInstanceEditor.removeTemplate = Remove
# Name of the standard AI agent
boxui.metadataInstanceEditor.standardAgentName = Standard
# Label to add a template
boxui.metadataInstanceEditor.templateAdd = Add
# Placeholder to search for all templates
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"@babel/types": "^7.24.7",
"@box/blueprint-web": "^12.7.1",
"@box/blueprint-web-assets": "^4.60.0",
"@box/box-ai-agent-selector": "^0.41.10",
"@box/box-ai-agent-selector": "^0.48.5",
"@box/box-ai-content-answers": "^0.124.1",
"@box/cldr-data": "^34.2.0",
"@box/combobox-with-api": "^0.34.9",
Expand Down
5 changes: 5 additions & 0 deletions src/common/types/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,15 @@ type MetadataCascadePolicy = {
cascadePolicyType?: string,
};

type MetadataCascadePolicyConfiguration = {
agent: string,
};

type MetadataCascadingPolicyData = {
id?: string,
isEnabled: boolean,
overwrite: boolean,
cascadePolicyConfiguration?: MetadataCascadePolicyConfiguration,
};

type MetadataInstance = {
Expand Down
71 changes: 48 additions & 23 deletions src/features/metadata-instance-editor/CascadePolicy.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// @flow
import * as React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';

import { InlineNotice } from '@box/blueprint-web';
import { BoxAiAgentSelector } from '@box/box-ai-agent-selector';
// $FlowFixMe
import { useCallback } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { BoxAiAdvancedColor } from '@box/blueprint-web-assets/icons/Medium';
import { type AgentType } from '@box/box-ai-agent-selector';

import { BoxAiAgentSelectorWithApiContainer } from '@box/box-ai-agent-selector';
import BoxAiLogo from '@box/blueprint-web-assets/icons/Logo/BoxAiLogo';

import Toggle from '../../components/toggle';
Expand All @@ -17,19 +20,6 @@ import './CascadePolicy.scss';
const COMMUNITY_LINK = 'https://support.box.com/hc/en-us/articles/360044195873-Cascading-metadata-in-folders';
const AI_LINK = 'https://www.box.com/ai';

const agents = [
{
id: '1',
name: 'Basic',
isEnterpriseDefault: true,
},
{
id: '2',
name: 'Enhanced (Gemini 2.5 Pro)',
isEnterpriseDefault: false,
},
];

type Props = {
canEdit: boolean,
canUseAIFolderExtraction: boolean,
Expand All @@ -40,6 +30,7 @@ type Props = {
isCustomMetadata: boolean,
isExistingCascadePolicy: boolean,
onAIFolderExtractionToggle: (value: boolean) => void,
onAIAgentSelect?: (agent: AgentType | null) => void,
onCascadeModeChange: (value: boolean) => void,
onCascadeToggle: (value: boolean) => void,
shouldShowCascadeOptions: boolean,
Expand All @@ -55,6 +46,7 @@ const CascadePolicy = ({
isAIFolderExtractionEnabled,
isExistingCascadePolicy,
onAIFolderExtractionToggle,
onAIAgentSelect,
onCascadeToggle,
onCascadeModeChange,
shouldShowCascadeOptions,
Expand All @@ -67,6 +59,39 @@ const CascadePolicy = ({
</div>
) : null;

const agents = React.useMemo(
() => [
{
id: '1',
name: formatMessage(messages.standardAgentName),
isEnterpriseDefault: true,
},
{
id: '2',
name: formatMessage(messages.enhancedAgentName),
isEnterpriseDefault: false,
customIcon: BoxAiAdvancedColor,
},
],
[formatMessage],
);

// BoxAiAgentSelectorWithApiContainer expects a function that returns a Promise<AgentListResponse>
// Since we're passing in our own agents, we don't need to make an API call,
// so we wrap the store data in a Promise to satisfy the component's interface requirements.
const agentFetcher = useCallback((): Promise<AgentListResponse> => {
return Promise.resolve({ agents });
}, [agents]);

const handleAgentSelect = useCallback(
(agent: AgentType | null) => {
if (onAIAgentSelect) {
onAIAgentSelect(agent);
}
},
[onAIAgentSelect],
);

return canEdit ? (
<>
{isExistingCascadePolicy && (
Expand All @@ -83,6 +108,7 @@ const CascadePolicy = ({
<FormattedMessage tagName="strong" {...messages.enableCascadePolicy} />
{!isCustomMetadata && (
<Toggle
aria-label={formatMessage(messages.enableCascadePolicy)}
className={`metadata-cascade-toggle ${
isCascadingEnabled ? 'cascade-on' : 'cascade-off'
}`}
Expand Down Expand Up @@ -144,6 +170,7 @@ const CascadePolicy = ({
<BoxAiLogo className="metadata-cascade-ai-logo" width={16} height={16} />
<FormattedMessage tagName="strong" {...messages.enableAIAutofill} />
<Toggle
aria-label={formatMessage(messages.enableAIAutofill)}
className="metadata-cascade-toggle"
isOn={isAIFolderExtractionEnabled}
isDisabled={isExistingCascadePolicy}
Expand All @@ -158,15 +185,13 @@ const CascadePolicy = ({
<FormattedMessage {...messages.aiAutofillLearnMore} />
</Link>
</div>
{canUseAIFolderExtractionAgentSelector && (
{canUseAIFolderExtractionAgentSelector && isAIFolderExtractionEnabled && (
<div className="metadata-cascade-ai-agent-selector">
<BoxAiAgentSelector
agents={agents}
<BoxAiAgentSelectorWithApiContainer
disabled={isExistingCascadePolicy}
onErrorAction={() => {}}
requestState="success"
selectedAgent={agents[0]}
variant="sidebar"
fetcher={agentFetcher}
onSelectAgent={handleAgentSelect}
recordAction={() => {}}
/>
</div>
)}
Expand Down
25 changes: 24 additions & 1 deletion src/features/metadata-instance-editor/Instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import MetadataInstanceConfirmDialog from './MetadataInstanceConfirmDialog';
import Footer from './Footer';
import messages from './messages';
import { FIELD_TYPE_FLOAT, FIELD_TYPE_INTEGER } from '../metadata-instance-fields/constants';
import { CASCADE_POLICY_TYPE_AI_EXTRACT, TEMPLATE_CUSTOM_PROPERTIES } from './constants';
import { CASCADE_POLICY_TYPE_AI_EXTRACT, TEMPLATE_CUSTOM_PROPERTIES, ENHANCED_AGENT_CONFIGURATION } from './constants';
import {
JSON_PATCH_OP_REMOVE,
JSON_PATCH_OP_ADD,
Expand All @@ -38,6 +38,7 @@ import type {
MetadataFields,
MetadataTemplate,
MetadataCascadePolicy,
MetadataCascadePolicyConfiguration,
MetadataCascadingPolicyData,
MetadataTemplateField,
MetadataFieldValue,
Expand Down Expand Up @@ -72,6 +73,7 @@ type State = {
data: Object,
errors: { [string]: React.Node },
isAIFolderExtractionEnabled: boolean,
cascadePolicyConfiguration: MetadataCascadePolicyConfiguration | null,
isBusy: boolean,
isCascadingEnabled: boolean,
isCascadingOverwritten: boolean,
Expand Down Expand Up @@ -217,6 +219,7 @@ class Instance extends React.PureComponent<Props, State> {
isAIFolderExtractionEnabled,
isCascadingEnabled,
isCascadingOverwritten,
cascadePolicyConfiguration,
}: State = this.state;

if (!this.isEditing() || !isDirty || !onSave || Object.keys(errors).length) {
Expand All @@ -229,6 +232,7 @@ class Instance extends React.PureComponent<Props, State> {
// reset state if cascading policy is removed
isAIFolderExtractionEnabled: isCascadingEnabled ? isAIFolderExtractionEnabled : false,
});

onSave(
id,
this.createJSONPatch(currentData, originalData),
Expand All @@ -239,6 +243,7 @@ class Instance extends React.PureComponent<Props, State> {
isEnabled: isCascadingEnabled,
overwrite: isCascadingOverwritten,
isAIFolderExtractionEnabled,
cascadePolicyConfiguration,
}
: undefined,
cloneDeep(currentData),
Expand Down Expand Up @@ -342,6 +347,23 @@ class Instance extends React.PureComponent<Props, State> {
this.setState({ isAIFolderExtractionEnabled: value }, this.setDirty);
};

/**
* Handles the selection of an AI agent
* @param {AgentType | null} agent - The selected agent
*/
onAIAgentSelect = (agent: AgentType | null): void => {
// '2' is the id for the enhanced agent
if (agent && agent.id === '2') {
this.setState({
cascadePolicyConfiguration: {
agent: ENHANCED_AGENT_CONFIGURATION,
},
});
} else {
this.setState({ cascadePolicyConfiguration: null });
}
};

/**
* Returns the state from props
*
Expand Down Expand Up @@ -684,6 +706,7 @@ class Instance extends React.PureComponent<Props, State> {
onAIFolderExtractionToggle={this.onAIFolderExtractionToggle}
onCascadeModeChange={this.onCascadeModeChange}
onCascadeToggle={this.onCascadeToggle}
onAIAgentSelect={this.onAIAgentSelect}
shouldShowCascadeOptions={shouldShowCascadeOptions}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,68 @@ describe('features/metadata-instance-editor/CascadePolicy', () => {
});

describe('AI Agent Selector', () => {
test('should render AI agent selector with default to basic when AI features are enabled', () => {
test('should render AI agent selector with default to basic when AI features are enabled', async () => {
render(
<CascadePolicy
canEdit
canUseAIFolderExtraction
canUseAIFolderExtractionAgentSelector
shouldShowCascadeOptions
isAIFolderExtractionEnabled
onAIFolderExtractionToggle={jest.fn()}
/>,
);
expect(screen.getByRole('combobox', { name: 'Basic' })).toBeInTheDocument();

const aiToggle = screen.getByRole('switch', { name: 'Box AI Autofill' });
await userEvent.click(aiToggle); // Enable AI

expect(aiToggle).toBeChecked();

expect(screen.getByRole('combobox', { name: 'Standard' })).toBeInTheDocument();
});

test('should not render AI agent selector when canUseAIFolderExtractionAgentSelector is false', () => {
render(<CascadePolicy canEdit canUseAIFolderExtraction shouldShowCascadeOptions />);
expect(screen.queryByRole('combobox', { name: 'Basic' })).not.toBeInTheDocument();
expect(screen.queryByRole('combobox', { name: 'Standard' })).not.toBeInTheDocument();
});

test('should call onAIAgentSelect when an agent is selected', async () => {
const onAIAgentSelect = jest.fn();
render(
<CascadePolicy
canEdit
canUseAIFolderExtraction
canUseAIFolderExtractionAgentSelector
shouldShowCascadeOptions
isAIFolderExtractionEnabled
onAIAgentSelect={onAIAgentSelect}
onAIFolderExtractionToggle={jest.fn()}
/>,
);

const aiToggle = screen.getByRole('switch', { name: 'Box AI Autofill' });
await userEvent.click(aiToggle); // Enable AI

expect(aiToggle).toBeChecked();

// Find the combobox by its accessible name
const combobox = screen.getByRole('combobox', { name: 'Standard' });

// Open the combobox (simulate user click)
await userEvent.click(combobox);

// Find the option for "Advanced" and select it
const option = await screen.findByRole('option', { name: 'Enhanced' });
await userEvent.click(option);

// The expected agent object (should match the one in CascadePolicy.js)
const expectedAgent = {
id: '1',
name: 'Standard',
isEnterpriseDefault: true,
};

expect(onAIAgentSelect).toHaveBeenCalledWith(expectedAgent);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,12 +863,30 @@ describe('Instance Component - React Testing Library', () => {

describe('Props passed to CascadePolicy', () => {
test('should pass canUseAIFolderExtractionAgentSelector to CascadePolicy', async () => {
render(<Instance {...getBaseProps({ canUseAIFolderExtractionAgentSelector: true })} />);
render(
<Instance
{...getBaseProps({
canUseAIFolderExtractionAgentSelector: true,
cascadePolicy: {
id: 'policy-1',
canEdit: true,
isEnabled: true,
cascadePolicyType: CASCADE_POLICY_TYPE_AI_EXTRACT,
},
})}
/>,
);

const editButton = screen.queryByRole('button', { name: 'Edit Metadata' });
if (editButton) await userEvent.click(editButton); // Enter edit mode to ensure CascadePolicy options are visible

expect(screen.getByRole('combobox', { name: 'Basic' })).toBeInTheDocument();
const cascadeToggle = screen.getByRole('switch', { name: 'Enable Cascade Policy' });
expect(cascadeToggle).toBeChecked();

const aiToggle = screen.getByRole('switch', { name: 'Box AI Autofill' });
expect(aiToggle).toBeChecked();

expect(screen.getByRole('combobox', { name: 'Standard' })).toBeInTheDocument();
});

test('should disable CascadePolicy options when a cascade already exists', async () => {
Expand Down
Loading