From 0b7fee0833ca4b4821e477ca36c356fe78349816 Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Wed, 25 Mar 2026 20:49:03 +0530 Subject: [PATCH 1/3] temp: prototyping file picker --- .../generic/table-components/TableActions.jsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/files-and-videos/generic/table-components/TableActions.jsx b/src/files-and-videos/generic/table-components/TableActions.jsx index 188de71315..d99477d07f 100644 --- a/src/files-and-videos/generic/table-components/TableActions.jsx +++ b/src/files-and-videos/generic/table-components/TableActions.jsx @@ -1,15 +1,10 @@ -import React, { useContext, useEffect } from 'react'; -import { isEmpty } from 'lodash'; -import { PropTypes } from 'prop-types'; -import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; -import { - Button, - DataTableContext, - Dropdown, - useToggle, -} from '@openedx/paragon'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { Button, DataTableContext, Dropdown, useToggle, } from '@openedx/paragon'; import { Add, Tune } from '@openedx/paragon/icons'; +import { isEmpty } from 'lodash'; +import { PropTypes } from 'prop-types'; +import React, { useContext, useEffect } from 'react'; import messages from '../messages'; import SortAndFilterModal from './sort-and-filter-modal'; @@ -26,12 +21,15 @@ const TableActions = ({ const intl = useIntl(); const [isSortOpen, openSort, closeSort] = useToggle(false); const { state, clearSelection } = useContext(DataTableContext); + const filePickerParams = new URLSearchParams(window.location.search); + const showFilePicker = Boolean(filePickerParams.get('filePicker')) && Boolean(window.opener); // This useEffect saves DataTable state so it can persist after table re-renders due to data reload. useEffect(() => { setInitialState(state); }, [state]); + const handleOpenFileSelector = () => { fileInputControl.click(); clearSelection(); @@ -80,6 +78,18 @@ const TableActions = ({ + {showFilePicker && ( + + )} ); From d12388f346edbc3454b4892014ebb4defd41b0ff Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Thu, 9 Apr 2026 02:47:21 +0530 Subject: [PATCH 2/3] feat: implement file picker mode for files page --- .../files-page/FilePickerPage.tsx | 20 +++++++++ .../{FilesPage.jsx => FilesPage.tsx} | 15 +++++-- .../files-page/FilesPageProvider.jsx | 25 ----------- .../files-page/FilesPageProvider.tsx | 41 +++++++++++++++++++ src/files-and-videos/generic/FileTable.jsx | 10 ++++- .../generic/table-components/TableActions.jsx | 11 +++-- src/index.jsx | 5 ++- src/store.ts | 19 ++++++++- 8 files changed, 109 insertions(+), 37 deletions(-) create mode 100644 src/files-and-videos/files-page/FilePickerPage.tsx rename src/files-and-videos/files-page/{FilesPage.jsx => FilesPage.tsx} (84%) delete mode 100644 src/files-and-videos/files-page/FilesPageProvider.jsx create mode 100644 src/files-and-videos/files-page/FilesPageProvider.tsx diff --git a/src/files-and-videos/files-page/FilePickerPage.tsx b/src/files-and-videos/files-page/FilePickerPage.tsx new file mode 100644 index 0000000000..e0e7c5aee0 --- /dev/null +++ b/src/files-and-videos/files-page/FilePickerPage.tsx @@ -0,0 +1,20 @@ +import { CourseAuthoringProvider } from '@src/CourseAuthoringContext'; +import { useLocation, useParams } from 'react-router-dom'; +import FilesPage from './FilesPage'; + +export const FilePickerPage = () => { + const { courseId } = useParams<{ courseId: string }>(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + const filePickerOptions = { + usageKey: params.get('usage_key')!, + multiSelect: params.get('multiSelect') === 'true', + mimeType: params.get('mimeType'), + }; + + return ( + + + + ); +}; diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.tsx similarity index 84% rename from src/files-and-videos/files-page/FilesPage.jsx rename to src/files-and-videos/files-page/FilesPage.tsx index 22f9e00980..01e08a2570 100644 --- a/src/files-and-videos/files-page/FilesPage.jsx +++ b/src/files-and-videos/files-page/FilesPage.tsx @@ -1,6 +1,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Container } from '@openedx/paragon'; +import { DeprecatedReduxState } from '@src/store'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -15,11 +16,17 @@ import { AgreementGated } from '@src/constants'; import { EditFileErrors } from '../generic'; import { fetchAssets, resetErrors } from './data/thunks'; -import FilesPageProvider from './FilesPageProvider'; +import FilesPageProvider, { FilePickerOptions } from './FilesPageProvider'; import messages from './messages'; import './FilesPage.scss'; -const FilesPage = () => { +const FilesPage = ({ + filePickerMode = false, + filePickerOptions = undefined, +}: { + filePickerMode?: boolean, + filePickerOptions?: FilePickerOptions, +}) => { const intl = useIntl(); const dispatch = useDispatch(); const { courseId, courseDetails } = useCourseAuthoringContext(); @@ -30,7 +37,7 @@ const FilesPage = () => { deletingStatus: deleteAssetStatus, updatingStatus: updateAssetStatus, errors: errorMessages, - } = useSelector(state => state.assets); + } = useSelector((state:DeprecatedReduxState) => state.assets); useEffect(() => { dispatch(fetchAssets(courseId)); @@ -47,7 +54,7 @@ const FilesPage = () => { } return ( - + { - const contextValue = useMemo(() => ({ - courseId, - path: `/course/${courseId}/assets`, - }), []); - return ( - - {children} - - ); -}; - -FilesPageProvider.propTypes = { - courseId: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -}; - -export default FilesPageProvider; diff --git a/src/files-and-videos/files-page/FilesPageProvider.tsx b/src/files-and-videos/files-page/FilesPageProvider.tsx new file mode 100644 index 0000000000..4af870e0c7 --- /dev/null +++ b/src/files-and-videos/files-page/FilesPageProvider.tsx @@ -0,0 +1,41 @@ +import React, { useMemo } from 'react'; + +export interface FilePickerOptions { + usageKey: string, + multiSelect: boolean, + mimeType: string | null, +} + +interface FilesPageContextInterface { + filePickerMode: boolean, + filePickerOptions?: FilePickerOptions, + +} + +export const FilesPageContext = React.createContext({ + filePickerMode: false, +}); + +interface FilesPageProviderProps extends FilesPageContextInterface { + children: React.ReactNode, +} + +const FilesPageProvider = ({ + children, + filePickerMode = false, + filePickerOptions, +}: FilesPageProviderProps) => { + const contextValue = useMemo(() => ({ + filePickerMode, + filePickerOptions, + }), []); + return ( + + {children} + + ); +}; + +export default FilesPageProvider; diff --git a/src/files-and-videos/generic/FileTable.jsx b/src/files-and-videos/generic/FileTable.jsx index 0dd632aacb..651798858f 100644 --- a/src/files-and-videos/generic/FileTable.jsx +++ b/src/files-and-videos/generic/FileTable.jsx @@ -1,4 +1,7 @@ -import { useCallback, useEffect, useState } from 'react'; +import { FilesPageContext } from '@src/files-and-videos/files-page/FilesPageProvider'; +import { + useCallback, useContext, useEffect, useState, +} from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import isEmpty from 'lodash/isEmpty'; @@ -11,7 +14,7 @@ import { useToggle, } from '@openedx/paragon'; -import { RequestStatus } from '../../data/constants'; +import { RequestStatus } from '@src/data/constants'; import { sortFiles } from './utils'; import messages from './messages'; @@ -79,6 +82,7 @@ const FileTable = ({ const defaultCurrentView = (fileType === 'video' && localStorage.getItem('videosCurrentView')) || (fileType === 'file' && localStorage.getItem('filesCurrentView')) || defaultView; const [currentView, setCurrentView] = useState(defaultCurrentView); + const { filePickerMode, filePickerOptions } = useContext(FilesPageContext); useEffect(() => { if (!isEmpty(selectedRows) && Object.keys(selectedRows[0]).length > 0) { @@ -189,6 +193,7 @@ const FileTable = ({ if (!hasMoreInfoColumn) { tableColumns.push({ ...moreInfoColumn }); } + const maxSelectedRows = filePickerOptions?.multiSelect === false ? 1 : undefined; return (
@@ -198,6 +203,7 @@ const FileTable = ({ isSortable isSelectable isPaginated + maxSelectedRows={maxSelectedRows} defaultColumnValues={{ Filter: TextFilter }} dataViewToggleOptions={{ isDataViewToggleEnabled: true, diff --git a/src/files-and-videos/generic/table-components/TableActions.jsx b/src/files-and-videos/generic/table-components/TableActions.jsx index d99477d07f..fdd8685dcb 100644 --- a/src/files-and-videos/generic/table-components/TableActions.jsx +++ b/src/files-and-videos/generic/table-components/TableActions.jsx @@ -1,7 +1,10 @@ import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { Button, DataTableContext, Dropdown, useToggle, } from '@openedx/paragon'; +import { + Button, DataTableContext, Dropdown, useToggle, +} from '@openedx/paragon'; import { Add, Tune } from '@openedx/paragon/icons'; +import FilesPageProvider, { FilesPageContext } from '@src/files-and-videos/files-page/FilesPageProvider'; import { isEmpty } from 'lodash'; import { PropTypes } from 'prop-types'; import React, { useContext, useEffect } from 'react'; @@ -21,15 +24,15 @@ const TableActions = ({ const intl = useIntl(); const [isSortOpen, openSort, closeSort] = useToggle(false); const { state, clearSelection } = useContext(DataTableContext); - const filePickerParams = new URLSearchParams(window.location.search); - const showFilePicker = Boolean(filePickerParams.get('filePicker')) && Boolean(window.opener); + const { filePickerMode } = useContext(FilesPageContext); + // If window.opener is not available, show the user some error message. + const showFilePicker = filePickerMode; // && Boolean(window.opener); // This useEffect saves DataTable state so it can persist after table re-renders due to data reload. useEffect(() => { setInitialState(state); }, [state]); - const handleOpenFileSelector = () => { fileInputControl.click(); clearSelection(); diff --git a/src/index.jsx b/src/index.jsx index b62a9e58cf..582df87956 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,7 +7,9 @@ import { getConfig, getPath, } from '@edx/frontend-platform'; -import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; +import { AppProvider, ErrorPage, PageWrap } from '@edx/frontend-platform/react'; +import { FilesPage } from '@src/files-and-videos'; +import { FilePickerPage } from '@src/files-and-videos/files-page/FilePickerPage'; import React, { StrictMode, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { @@ -102,6 +104,7 @@ const App = () => { } /> } /> } /> + } /> {getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && ( } /> )} diff --git a/src/store.ts b/src/store.ts index 99d9331b30..449ba3ad8e 100644 --- a/src/store.ts +++ b/src/store.ts @@ -33,7 +33,24 @@ type InferState = ReducerType extends Reducer ? T : never; export interface DeprecatedReduxState { customPages: Record; discussions: Record; - assets: Record; + assets: { + assetIds: string[]; + loadingStatus: RequestStatusType; + duplicateFiles: string[]; + updatingStatus: string; + addingStatus: string; + deletingStatus: string; + usageStatus: string; + errors: { + add: string[]; + delete: string[]; + lock: string[]; + download: string[]; + usageMetrics: string[]; + loading:string; + + }; + }; pagesAndResources: Record; scheduleAndDetails: Record; studioHome: InferState; From 308762cadbe711f94970b1b163a81cc92363a6fb Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Wed, 22 Apr 2026 01:19:43 +0530 Subject: [PATCH 3/3] fixup! feat: implement file picker mode for files page --- src/files-and-videos/files-page/FilePickerPage.tsx | 2 +- src/files-and-videos/files-page/FilesPage.tsx | 6 +++--- .../files-page/FilesPageProvider.tsx | 13 ++++++------- src/files-and-videos/generic/FileTable.jsx | 7 +++++-- .../generic/table-components/TableActions.jsx | 12 +++++++++--- src/index.jsx | 10 ++++++++-- src/store.ts | 3 +-- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/files-and-videos/files-page/FilePickerPage.tsx b/src/files-and-videos/files-page/FilePickerPage.tsx index e0e7c5aee0..bd818fdfdc 100644 --- a/src/files-and-videos/files-page/FilePickerPage.tsx +++ b/src/files-and-videos/files-page/FilePickerPage.tsx @@ -3,7 +3,7 @@ import { useLocation, useParams } from 'react-router-dom'; import FilesPage from './FilesPage'; export const FilePickerPage = () => { - const { courseId } = useParams<{ courseId: string }>(); + const { courseId } = useParams<{ courseId: string; }>(); const location = useLocation(); const params = new URLSearchParams(location.search); const filePickerOptions = { diff --git a/src/files-and-videos/files-page/FilesPage.tsx b/src/files-and-videos/files-page/FilesPage.tsx index 01e08a2570..15a28b892c 100644 --- a/src/files-and-videos/files-page/FilesPage.tsx +++ b/src/files-and-videos/files-page/FilesPage.tsx @@ -24,8 +24,8 @@ const FilesPage = ({ filePickerMode = false, filePickerOptions = undefined, }: { - filePickerMode?: boolean, - filePickerOptions?: FilePickerOptions, + filePickerMode?: boolean; + filePickerOptions?: FilePickerOptions; }) => { const intl = useIntl(); const dispatch = useDispatch(); @@ -37,7 +37,7 @@ const FilesPage = ({ deletingStatus: deleteAssetStatus, updatingStatus: updateAssetStatus, errors: errorMessages, - } = useSelector((state:DeprecatedReduxState) => state.assets); + } = useSelector((state: DeprecatedReduxState) => state.assets); useEffect(() => { dispatch(fetchAssets(courseId)); diff --git a/src/files-and-videos/files-page/FilesPageProvider.tsx b/src/files-and-videos/files-page/FilesPageProvider.tsx index 4af870e0c7..440320bd1e 100644 --- a/src/files-and-videos/files-page/FilesPageProvider.tsx +++ b/src/files-and-videos/files-page/FilesPageProvider.tsx @@ -1,15 +1,14 @@ import React, { useMemo } from 'react'; export interface FilePickerOptions { - usageKey: string, - multiSelect: boolean, - mimeType: string | null, + usageKey: string; + multiSelect: boolean; + mimeType: string | null; } interface FilesPageContextInterface { - filePickerMode: boolean, - filePickerOptions?: FilePickerOptions, - + filePickerMode: boolean; + filePickerOptions?: FilePickerOptions; } export const FilesPageContext = React.createContext({ @@ -17,7 +16,7 @@ export const FilesPageContext = React.createContext({ }); interface FilesPageProviderProps extends FilesPageContextInterface { - children: React.ReactNode, + children: React.ReactNode; } const FilesPageProvider = ({ diff --git a/src/files-and-videos/generic/FileTable.jsx b/src/files-and-videos/generic/FileTable.jsx index 651798858f..db5e45bc98 100644 --- a/src/files-and-videos/generic/FileTable.jsx +++ b/src/files-and-videos/generic/FileTable.jsx @@ -1,6 +1,9 @@ import { FilesPageContext } from '@src/files-and-videos/files-page/FilesPageProvider'; import { - useCallback, useContext, useEffect, useState, + useCallback, + useContext, + useEffect, + useState, } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; @@ -82,7 +85,7 @@ const FileTable = ({ const defaultCurrentView = (fileType === 'video' && localStorage.getItem('videosCurrentView')) || (fileType === 'file' && localStorage.getItem('filesCurrentView')) || defaultView; const [currentView, setCurrentView] = useState(defaultCurrentView); - const { filePickerMode, filePickerOptions } = useContext(FilesPageContext); + const { filePickerOptions } = useContext(FilesPageContext); useEffect(() => { if (!isEmpty(selectedRows) && Object.keys(selectedRows[0]).length > 0) { diff --git a/src/files-and-videos/generic/table-components/TableActions.jsx b/src/files-and-videos/generic/table-components/TableActions.jsx index fdd8685dcb..cd2a339aba 100644 --- a/src/files-and-videos/generic/table-components/TableActions.jsx +++ b/src/files-and-videos/generic/table-components/TableActions.jsx @@ -1,10 +1,13 @@ import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { - Button, DataTableContext, Dropdown, useToggle, + Button, + DataTableContext, + Dropdown, + useToggle, } from '@openedx/paragon'; import { Add, Tune } from '@openedx/paragon/icons'; -import FilesPageProvider, { FilesPageContext } from '@src/files-and-videos/files-page/FilesPageProvider'; +import { FilesPageContext } from '@src/files-and-videos/files-page/FilesPageProvider'; import { isEmpty } from 'lodash'; import { PropTypes } from 'prop-types'; import React, { useContext, useEffect } from 'react'; @@ -85,7 +88,10 @@ const TableActions = ({