Skip to content
Closed
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
6 changes: 6 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,12 @@ boxui.metadataInstanceEditor.customErrorDuplicateKey = A field with that key alr
boxui.metadataInstanceEditor.customErrorInternalKey = Keys cannot begin with a $.
# Error enforcing required key for custom metadata
boxui.metadataInstanceEditor.customErrorRequired = A key is required.
# Label for the button that navigates the user to manage the custom Box AI extract agent
boxui.metadataInstanceEditor.customExtractAgentManageButton = Manage agent
# Body of the notice shown when a metadata instance is managed by a custom Box AI extract agent
boxui.metadataInstanceEditor.customExtractAgentNoticeDescription = This policy is managed by an agent. Manage the agent to change the configuration.
# Aria label for the info icon on the custom extract agent notice
boxui.metadataInstanceEditor.customExtractAgentNoticeIconAriaLabel = Locked
# Label for the key field for custom metadata
boxui.metadataInstanceEditor.customKey = Key
# Placeholder for the key field for custom metadata
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @flow
import * as React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';

import { ActionableInlineNotice } from '@box/blueprint-web';
// $FlowFixMe - blueprint-web-assets icons not typed for Flow
import { Lock } from '@box/blueprint-web-assets/icons/Line';

import TemplatedInstance from './TemplatedInstance';
import CustomInstance from './CustomInstance';

import { getCustomExtractAgentId } from './metadataUtil';

import type { MetadataFields, MetadataTemplate } from '../../common/types/metadata';
Comment thread
reneshen0328 marked this conversation as resolved.

import { TEMPLATE_CUSTOM_PROPERTIES } from './constants';

import messages from './messages';

import './CustomExtractAgentInstanceBody.scss';

type Props = {
// Raw agent configuration value from the cascade policy (e.g. `extract_agent_<id>`).
// The navigable numeric id is extracted from this value via getCustomExtractAgentId.
agentConfiguration?: string,
data: MetadataFields,
isEditing: boolean,
onManageExtractAgent?: (agentId: string) => void,
template: MetadataTemplate,
};

/**
* Presentational interior for a metadata instance managed by a custom Box AI
* extract agent. In read-only (view) mode it shows the field values without edit
* controls; when the user enters edit mode it replaces the form with an informational
* notice and a button to manage the agent (instead of allowing inline edits).
*
* The "manage agent" button is only shown when a navigable numeric agent id can be
* resolved from the configuration; otherwise the notice is shown without an action.
*/
const CustomExtractAgentInstanceBody = ({
agentConfiguration,
data,
isEditing,
onManageExtractAgent,
template,
}: Props) => {
const { formatMessage } = useIntl();
const isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES;
const customExtractAgentId = getCustomExtractAgentId(agentConfiguration);

if (isEditing) {
return (
<div className="metadata-instance-editor-custom-extract-agent">
<ActionableInlineNotice
icon={Lock}
iconAriaLabel={formatMessage(messages.customExtractAgentNoticeIconAriaLabel)}
text={formatMessage(messages.customExtractAgentNoticeDescription)}
>
{!!customExtractAgentId && onManageExtractAgent && (
<ActionableInlineNotice.PrimaryAction
onClick={() => onManageExtractAgent(customExtractAgentId)}
>
{formatMessage(messages.customExtractAgentManageButton)}
</ActionableInlineNotice.PrimaryAction>
)}
</ActionableInlineNotice>
</div>
);
}

return (
<div className="metadata-instance-editor-instance">
<div className="metadata-cascade-notice">
<FormattedMessage {...messages.metadataCascadePolicyEnabledInfo} />
</div>
{isProperties ? (
<CustomInstance canEdit={false} data={data} />
) : (
<TemplatedInstance canEdit={false} data={data} errors={{}} template={template} />
)}
</div>
);
};

export default CustomExtractAgentInstanceBody;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.metadata-instance-editor-custom-extract-agent {
padding: var(--bp-space-020);
}
146 changes: 146 additions & 0 deletions src/features/metadata-instance-editor/EditableInstanceBody.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// @flow
import * as React from 'react';
import noop from 'lodash/noop';

import type { AgentType } from '@box/box-ai-agent-selector';

import Form from '../../components/form-elements/form/Form';
import LoadingIndicatorWrapper from '../../components/loading-indicator/LoadingIndicatorWrapper';
import CascadePolicy from './CascadePolicy';
import TemplatedInstance from './TemplatedInstance';
import CustomInstance from './CustomInstance';
import MetadataInstanceConfirmDialog from './MetadataInstanceConfirmDialog';
import Footer from './Footer';

import type {
MetadataCascadePolicy,
MetadataFields,
MetadataTemplate,
MetadataFieldValue,
} from '../../common/types/metadata';

type Props = {
canUseAIFolderExtraction: boolean,
cascadePolicy: MetadataCascadePolicy,
confirmationMessage: React.Node,
data: MetadataFields,
errors: { [string]: React.Node },
isAIFolderExtractionEnabled: boolean,
isBusy: boolean,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename the isBusy prop to isLoading here? It's only passed to <LoadingIndicatorWrapper isLoading={...}>, so matching the name makes the intent clearer. The parent can still pass isLoading={isBusy} — no need to rename the state variable in Instance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is not new, but an old component that I moved. I would like to avoid unnecessary refactoring in this PR

isCascadingEnabled: boolean,
isCascadingOverwritten: boolean,
isCascadingPolicyApplicable?: boolean,
isDirty: boolean,
isEditing: boolean,
isExistingCascadePolicy: boolean,
isProperties: boolean,
onAIAgentSelect: (agent: AgentType | null) => void,
onAIFolderExtractionToggle: (value: boolean) => void,
onCancel: () => void,
onCascadeModeChange: (value: boolean) => void,
onCascadeToggle: (value: boolean) => void,
onConfirmCancel: () => void,
onConfirmRemove: () => void,
onFieldChange: (key: string, value: MetadataFieldValue, type: string) => void,
onFieldRemove: (key: string) => void,
onRemove: () => void,
onSave: () => void,
shouldConfirmRemove: boolean,
shouldShowCascadeOptions: boolean,
template: MetadataTemplate,
};

/**
* Presentational interior for an editable metadata instance: the confirm-remove
* dialog, the form with cascade policy + fields, and the save/remove footer.
* All state and handlers are owned by the parent Instance and supplied via props.
*/
const EditableInstanceBody = ({
canUseAIFolderExtraction,
cascadePolicy = {},
confirmationMessage,
data,
errors,
isAIFolderExtractionEnabled,
isBusy,
isCascadingEnabled,
isCascadingOverwritten,
isCascadingPolicyApplicable,
isDirty,
isEditing,
isExistingCascadePolicy,
isProperties,
onAIAgentSelect,
onAIFolderExtractionToggle,
onCancel,
onCascadeModeChange,
onCascadeToggle,
onConfirmCancel,
onConfirmRemove,
onFieldChange,
onFieldRemove,
onRemove,
onSave,
shouldConfirmRemove,
shouldShowCascadeOptions,
template,
}: Props) => {
if (shouldConfirmRemove) {
return (
<LoadingIndicatorWrapper isLoading={isBusy}>
<MetadataInstanceConfirmDialog
confirmationMessage={confirmationMessage}
onCancel={onConfirmCancel}
onConfirm={onRemove}
/>
</LoadingIndicatorWrapper>
);
}
Comment on lines +88 to +98

@reneshen0328 reneshen0328 Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The shouldConfirmRemove early return on line 87 means this component has two completely unrelated render paths. A cleaner split would be having Instance.js handles the rendering logic, the same way it already chooses between <CustomExtractAgentInstanceBody> and <EditableInstanceBody>. This would also remove 4 props (shouldConfirmRemove, confirmationMessage, onConfirmCancel, onRemove) for this component.
  2. Since EditableInstanceBody is only used here and isBusy is owned by Instance, consider wrapping it with LoadingIndicatorWrapper at this level instead of inside the body. That way the body stays purely presentational and you can drop the isBusy prop from its interface.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you might be right on this, but I would like to avoid making any refactoring/improvements for existing code. I know it shows as a new component, but I actually just extracted it from existing one

  • remove confirmation is not related to a CustomExtractAgent, since it's not possible to remove an instance created by extract agent

I know a code is not golden example, but in scope of BUIE I believe smaller steps are the better


return (
<LoadingIndicatorWrapper isLoading={isBusy}>
<Form onValidSubmit={isDirty ? onSave : noop}>
<div className="metadata-instance-editor-instance">
{isCascadingPolicyApplicable && (
<CascadePolicy
cascadePolicyConfiguration={cascadePolicy?.cascadePolicyConfiguration}
canEdit={isEditing && !!cascadePolicy.canEdit}
canUseAIFolderExtraction={canUseAIFolderExtraction}
isAIFolderExtractionEnabled={isAIFolderExtractionEnabled}
isCascadingEnabled={isCascadingEnabled}
isCascadingOverwritten={isCascadingOverwritten}
isCustomMetadata={isProperties}
isExistingCascadePolicy={isExistingCascadePolicy}
onAIAgentSelect={onAIAgentSelect}
onAIFolderExtractionToggle={onAIFolderExtractionToggle}
onCascadeModeChange={onCascadeModeChange}
onCascadeToggle={onCascadeToggle}
shouldShowCascadeOptions={shouldShowCascadeOptions}
/>
)}
{isProperties ? (
<CustomInstance
canEdit={isEditing}
data={data}
onFieldChange={onFieldChange}
onFieldRemove={onFieldRemove}
/>
) : (
<TemplatedInstance
canEdit={isEditing}
data={data}
errors={errors}
isDisabled={isAIFolderExtractionEnabled}
onFieldChange={onFieldChange}
onFieldRemove={onFieldRemove}
template={template}
/>
)}
</div>
{isEditing && <Footer onCancel={onCancel} onRemove={onConfirmRemove} showSave={isDirty} />}
</Form>
</LoadingIndicatorWrapper>
);
};

export default EditableInstanceBody;
Loading
Loading