Skip to content

Commit eac7302

Browse files
dfenskeclaude
andcommitted
feat: Add move functionality and component customization support
This commit applies custom patches to support move operations and component customization in the content explorer and picker components. Changes: - Add move functionality for files/folders with proper UI feedback - Added canMove prop and onItemMove callback to ItemOptions - Added move message for internationalization - Prevent moving items into themselves - Filter out items being moved from item lists - Enable component customization - Add component override system (Header, SubHeader, Content, CreateFolderDialog) - Support custom headers/subheaders in ContentPicker - Add headerActionButtons prop for additional header actions - Improve UX for move operations - Change button text from "Choose" to "Move" in VIEW_SELECTED mode - Toggle between VIEW_SELECTED and VIEW_FOLDER states - Pass itemBeingMovedId through component tree for consistent state - Update ContentExplorer with new props: - components, headerActionButtons, itemBeingMovedId, move, moveCallback - Swap Header and SubHeader positions for better layout These changes enable the box-ui-elements fork to support advanced file/folder management operations while maintaining backward compatibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 9b89baa commit eac7302

File tree

5 files changed

+123
-29
lines changed

5 files changed

+123
-29
lines changed

src/elements/content-explorer/ContentExplorer.tsx

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,20 @@ export interface ContentExplorerProps {
123123
canShare?: boolean;
124124
canUpload?: boolean;
125125
className?: string;
126+
components?: {
127+
Content?: React.ComponentType<any>;
128+
CreateFolderDialog?: React.ComponentType<any>;
129+
Header?: React.ComponentType<any>;
130+
SubHeader?: React.ComponentType<any>;
131+
};
126132
contentPreviewProps?: ContentPreviewProps;
127133
contentUploaderProps?: ContentUploaderProps;
128134
currentFolderId?: string;
129135
defaultView?: DefaultView;
130136
features?: FeatureConfig;
131137
fieldsToShow?: FieldsToShow;
132138
hasProviders?: boolean;
139+
headerActionButtons?: React.ReactNode[];
133140
initialPage?: number;
134141
initialPageSize?: number;
135142
isLarge?: boolean;
@@ -138,6 +145,7 @@ export interface ContentExplorerProps {
138145
isTouch?: boolean;
139146
isVeryLarge?: boolean;
140147
itemActions?: ItemAction[];
148+
itemBeingMovedId?: string;
141149
language?: string;
142150
logoUrl?: string;
143151
measureRef?: (ref: Element | null) => void;
@@ -147,12 +155,15 @@ export interface ContentExplorerProps {
147155
MetadataViewContainerProps,
148156
'hasError' | 'currentCollection' | 'metadataTemplate' | 'selectedKeys'
149157
>;
158+
move?: (item: BoxItem) => void;
159+
moveCallback?: () => void;
150160
onCreate?: (item: BoxItem) => void;
151161
onDelete?: (item: BoxItem) => void;
152162
onDownload?: (item: BoxItem) => void;
153163
onNavigate?: (item: BoxItem) => void;
154164
onPreview?: (data: unknown) => void;
155165
onRename?: (item: BoxItem) => void;
166+
onSearch?: (query: string) => void;
156167
onSelect?: (item: BoxItem) => void;
157168
onUpload?: (item: BoxItem) => void;
158169
previewLibraryVersion?: string;
@@ -188,6 +199,7 @@ type State = {
188199
isCreateFolderModalOpen: boolean;
189200
isDeleteModalOpen: boolean;
190201
isLoading: boolean;
202+
isMoveModalOpen: boolean;
191203
isPreviewModalOpen: boolean;
192204
isRenameModalOpen: boolean;
193205
isShareModalOpen: boolean;
@@ -254,6 +266,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
254266
onSelect: noop,
255267
onUpload: noop,
256268
onNavigate: noop,
269+
onSearch: noop,
257270
defaultView: DEFAULT_VIEW_FILES,
258271
initialPage: DEFAULT_PAGE_NUMBER,
259272
initialPageSize: DEFAULT_PAGE_SIZE,
@@ -316,6 +329,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
316329
isCreateFolderModalOpen: false,
317330
isDeleteModalOpen: false,
318331
isLoading: false,
332+
isMoveModalOpen: false,
319333
sidePanelState: SidePanelState.CLOSED,
320334
isPreviewModalOpen: false,
321335
isRenameModalOpen: false,
@@ -771,6 +785,15 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
771785
debouncedSearch: ReturnType<typeof debounce> = debounce((id: string, query: string) => {
772786
const { currentOffset, currentPageSize }: State = this.state;
773787

788+
const {
789+
onSearch
790+
} = this.props;
791+
792+
// Call the onSearch callback if provided
793+
if (onSearch) {
794+
onSearch(query);
795+
}
796+
774797
this.api
775798
.getSearchAPI()
776799
.search(id, query, currentPageSize, currentOffset, this.searchSuccessCallback, this.errorCallback, {
@@ -1217,10 +1240,13 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
12171240
this.select(item, this.deleteCallback);
12181241
};
12191242

1220-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1221-
move = (_item: BoxItem): void => {
1222-
// Placeholder move method - does nothing by default
1223-
// Can be extended by consumers if needed
1243+
move = (item: BoxItem): void => {
1244+
const { move } = this.props;
1245+
if (move) {
1246+
this.select(item, () => move(item));
1247+
} else {
1248+
this.setState({ isMoveModalOpen: true });
1249+
}
12241250
};
12251251

12261252
/**
@@ -1516,6 +1542,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
15161542
this.setState({
15171543
isLoading: false,
15181544
isDeleteModalOpen: false,
1545+
isMoveModalOpen: false,
15191546
isRenameModalOpen: false,
15201547
isCreateFolderModalOpen: false,
15211548
isShareModalOpen: false,
@@ -1835,15 +1862,18 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
18351862
canShare,
18361863
canUpload,
18371864
className,
1865+
components = {},
18381866
contentPreviewProps,
18391867
contentUploaderProps,
18401868
defaultView,
18411869
features,
18421870
hasProviders,
1871+
headerActionButtons,
18431872
isMedium,
18441873
isSmall,
18451874
isTouch,
18461875
itemActions,
1876+
itemBeingMovedId,
18471877
language,
18481878
logoUrl,
18491879
measureRef,
@@ -1908,6 +1938,14 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
19081938
const isSidePanelOpen = sidePanelState !== SidePanelState.CLOSED;
19091939
const isEditing = sidePanelState === SidePanelState.EDITING;
19101940

1941+
// Extract component overrides or use defaults
1942+
const {
1943+
Content: CustomContent = Content,
1944+
CreateFolderDialog: CustomCreateFolderDialog = CreateFolderDialog,
1945+
Header: CustomHeader = Header,
1946+
SubHeader: CustomSubHeader = SubHeader,
1947+
} = components;
1948+
19111949
/* eslint-disable jsx-a11y/no-static-element-interactions */
19121950
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
19131951
return (
@@ -1917,11 +1955,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
19171955
<ThemingStyles selector={`#${this.id}`} theme={theme} />
19181956
<div className="be-app-element" onKeyDown={this.onKeyDown} tabIndex={0}>
19191957
<div className="bce-ContentExplorer-main">
1920-
{!isDefaultViewMetadata && (
1921-
<Header view={view} logoUrl={logoUrl} onSearch={this.search} />
1922-
)}
1923-
1924-
<SubHeader
1958+
<CustomSubHeader
19251959
bulkItemActions={bulkItemActions}
19261960
view={view}
19271961
viewMode={viewMode}
@@ -1946,12 +1980,17 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
19461980
portalElement={this.rootElement}
19471981
selectedItemIds={selectedItemIds}
19481982
title={title}
1983+
headerActionButtons={headerActionButtons}
19491984
/>
19501985

1951-
<Content
1986+
{!isDefaultViewMetadata && (
1987+
<CustomHeader view={view} logoUrl={logoUrl} onSearch={this.search} />
1988+
)}
1989+
1990+
<CustomContent
19521991
canDelete={canDelete}
19531992
canDownload={canDownload}
1954-
canMove={false}
1993+
canMove={!!this.props.move}
19551994
canPreview={canPreview}
19561995
canRename={canRename}
19571996
canShare={canShare}
@@ -1963,6 +2002,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
19632002
isSmall={isSmall}
19642003
isTouch={isTouch}
19652004
itemActions={itemActions}
2005+
itemBeingMovedId={itemBeingMovedId}
19662006
fieldsToShow={fieldsToShow}
19672007
metadataTemplate={metadataTemplate}
19682008
metadataViewProps={metadataViewProps}
@@ -2029,7 +2069,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
20292069
/>
20302070
) : null}
20312071
{allowCreate && !!this.appElement ? (
2032-
<CreateFolderDialog
2072+
<CustomCreateFolderDialog
20332073
isOpen={isCreateFolderModalOpen}
20342074
onCreate={this.throttledCreateFolderCallback}
20352075
onCancel={this.closeModals}

src/elements/content-picker/Content.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
import * as React from 'react';
88
// $FlowFixMe TypeScript file
9+
import { VIEW_ERROR, VIEW_SELECTED } from '../../constants';
910
import EmptyView from '../common/empty-view';
1011
import ProgressBar from '../common/progress-bar';
1112
import ItemList from './ItemList';
12-
import { VIEW_ERROR, VIEW_SELECTED } from '../../constants';
1313
import type { View, Collection } from '../../common/types/core';
1414
// $FlowFixMe TypeScript file
1515
import type { ItemAction } from '../common/item';
@@ -25,6 +25,7 @@ type Props = {
2525
isSingleSelect: boolean,
2626
isSmall: boolean,
2727
itemActions?: ItemAction[],
28+
itemBeingMovedId?: string,
2829
onFocusChange: Function,
2930
onItemClick: Function,
3031
onItemSelect: Function,
@@ -66,6 +67,7 @@ const Content = ({
6667
onFocusChange,
6768
extensionsWhitelist,
6869
itemActions,
70+
itemBeingMovedId,
6971
}: Props) => (
7072
<div className="bcp-content">
7173
{view === VIEW_ERROR || view === VIEW_SELECTED ? null : (
@@ -92,6 +94,7 @@ const Content = ({
9294
onShareAccessChange={onShareAccessChange}
9395
extensionsWhitelist={extensionsWhitelist}
9496
itemActions={itemActions}
97+
itemBeingMovedId={itemBeingMovedId}
9598
/>
9699
)}
97100
</div>

src/elements/content-picker/ContentPicker.js

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ class ContentPicker extends Component<Props, State> {
527527
* @return {void}
528528
*/
529529
fetchFolder = (id?: string, triggerNavigationEvent?: boolean = true): void => {
530-
const { rootFolderId }: Props = this.props;
530+
const { itemBeingMovedId, onSelect, rootFolderId }: Props = this.props;
531531
const {
532532
currentCollection: { id: currentId },
533533
currentOffset,
@@ -537,6 +537,10 @@ class ContentPicker extends Component<Props, State> {
537537
sortDirection,
538538
}: State = this.state;
539539
const folderId: string = typeof id === 'string' ? id : rootFolderId;
540+
if (itemBeingMovedId && folderId === itemBeingMovedId) {
541+
return;
542+
}
543+
onSelect(folderId);
540544
const hasFolderChanged = currentId && currentId !== folderId;
541545
const hasSearchQuery = !!searchQuery.trim().length;
542546
const offset = hasFolderChanged || hasSearchQuery ? 0 : currentOffset; // Reset offset on folder or mode change
@@ -565,6 +569,7 @@ class ContentPicker extends Component<Props, State> {
565569
sortBy,
566570
sortDirection,
567571
(collection: Collection) => {
572+
onSelect(collection);
568573
this.fetchFolderSuccessCallback(collection, triggerNavigationEvent);
569574
},
570575
this.errorCallback,
@@ -627,12 +632,18 @@ class ContentPicker extends Component<Props, State> {
627632
* @return {void}
628633
*/
629634
showSelected = (): void => {
630-
const { selected, sortBy, sortDirection }: State = this.state;
635+
const { selected, sortBy, sortDirection, view }: State = this.state;
636+
const { rootFolderId }: Props = this.props;
637+
if (view === VIEW_SELECTED) {
638+
this.setState({ currentOffset: 0 }, () => {
639+
this.fetchFolder(rootFolderId, false);
640+
});
641+
}
631642
this.setState(
632643
{
633644
searchQuery: '',
634-
view: VIEW_SELECTED,
635-
currentCollection: {
645+
view: view === VIEW_SELECTED ? VIEW_FOLDER : VIEW_SELECTED,
646+
currentCollection: view === VIEW_SELECTED ? {} : {
636647
sortBy,
637648
sortDirection,
638649
percentLoaded: 100,
@@ -1220,6 +1231,10 @@ class ContentPicker extends Component<Props, State> {
12201231
showSelectedButton,
12211232
theme,
12221233
itemActions,
1234+
itemBeingMovedId,
1235+
customHeader,
1236+
customSubHeader,
1237+
onUpload,
12231238
}: Props = this.props;
12241239
const {
12251240
view,
@@ -1250,14 +1265,39 @@ class ContentPicker extends Component<Props, State> {
12501265
<div id={this.id} className={styleClassName} ref={measureRef} data-testid="content-picker">
12511266
<ThemingStyles theme={theme} />
12521267
<div className="be-app-element" onKeyDown={this.onKeyDown} tabIndex={0}>
1253-
<Header
1254-
view={view}
1255-
isHeaderLogoVisible={isHeaderLogoVisible}
1256-
searchQuery={searchQuery}
1257-
logoUrl={logoUrl}
1258-
onSearch={this.search}
1259-
/>
1260-
<SubHeader
1268+
{customHeader ? (
1269+
<customHeader
1270+
view={view}
1271+
onSearch={this.search}
1272+
currentCollection={currentCollection}
1273+
/>
1274+
) : (
1275+
<Header
1276+
view={view}
1277+
isHeaderLogoVisible={isHeaderLogoVisible}
1278+
searchQuery={searchQuery}
1279+
logoUrl={logoUrl}
1280+
onSearch={this.search}
1281+
/>
1282+
)}
1283+
{customSubHeader ? (
1284+
<customSubHeader
1285+
view="folder"
1286+
showButtons={false}
1287+
canCreateNewFolder={false}
1288+
canUpload={allowUpload}
1289+
currentCollection={currentCollection}
1290+
headerActionButtons={false}
1291+
isSmall={isSmall}
1292+
onCreate={noop}
1293+
onItemClick={this.fetchFolder}
1294+
onUpload={this.upload}
1295+
onViewModeChange={noop}
1296+
rootName={rootName}
1297+
rootId={rootFolderId}
1298+
/>
1299+
) : (
1300+
<SubHeader
12611301
view={view}
12621302
rootId={rootFolderId}
12631303
isSmall={isSmall}
@@ -1270,6 +1310,7 @@ class ContentPicker extends Component<Props, State> {
12701310
onItemClick={this.fetchFolder}
12711311
onSortChange={this.sort}
12721312
/>
1313+
)}
12731314
<Content
12741315
view={view}
12751316
isSmall={isSmall}
@@ -1288,6 +1329,7 @@ class ContentPicker extends Component<Props, State> {
12881329
onFocusChange={this.onFocusChange}
12891330
onShareAccessChange={this.changeShareAccess}
12901331
itemActions={itemActions}
1332+
itemBeingMovedId={itemBeingMovedId}
12911333
/>
12921334
<Footer
12931335
currentCollection={currentCollection}
@@ -1302,6 +1344,7 @@ class ContentPicker extends Component<Props, State> {
13021344
chooseButtonLabel={chooseButtonLabel}
13031345
cancelButtonLabel={cancelButtonLabel}
13041346
renderCustomActionButtons={renderCustomActionButtons}
1347+
view={view}
13051348
>
13061349
{isPaginationVisible ? (
13071350
<Pagination
@@ -1328,6 +1371,7 @@ class ContentPicker extends Component<Props, State> {
13281371
contentUploaderProps={contentUploaderProps}
13291372
requestInterceptor={requestInterceptor}
13301373
responseInterceptor={responseInterceptor}
1374+
onUpload={onUpload}
13311375
/>
13321376
) : null}
13331377
{allowCreate && !!this.appElement ? (

0 commit comments

Comments
 (0)