diff --git a/client/app/api/course/Material/Folders.ts b/client/app/api/course/Material/Folders.ts index 0088b53fd47..4e577ddf0c3 100644 --- a/client/app/api/course/Material/Folders.ts +++ b/client/app/api/course/Material/Folders.ts @@ -51,6 +51,27 @@ export default class FoldersAPI extends BaseCourseAPI { ); } + /** + * Chunks a material (file) + */ + chunkMaterial( + currFolderId: number, + materialId: number, + ): APIResponse { + return this.client.put( + `${this.#urlPrefix}/${currFolderId}/files/${materialId}/create_text_chunks`, + ); + } + + /** + * Deletes Chunks associated with a material (file) + */ + deleteMaterialChunks(currFolderId: number, materialId: number): APIResponse { + return this.client.delete( + `${this.#urlPrefix}/${currFolderId}/files/${materialId}/destroy_text_chunks`, + ); + } + /** * Uploads materials (files) */ diff --git a/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx b/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx index b8ff6a88b13..df8aae0a5ea 100644 --- a/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx +++ b/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx @@ -20,6 +20,7 @@ interface Props { canEdit: boolean; canDelete: boolean; type: 'subfolder' | 'material'; + state: 'not_chunked' | 'chunking' | 'chunked' | null; folderInitialValues?: { name: string; description: string; @@ -61,6 +62,7 @@ const WorkbinTableButtons: FC = (props) => { isConcrete, canEdit, canDelete, + state, type, folderInitialValues, materialInitialValues, @@ -147,6 +149,7 @@ const WorkbinTableButtons: FC = (props) => { {canEdit && isConcrete && ( = (props) => { confirmMessage={`${t( translations.deleteConfirmation, )} "${itemName}"`} - disabled={isDeleting} + disabled={isDeleting || state === 'chunking'} id={`${type}-delete-button-${itemId}`} onClick={onDelete} style={{ padding: 5 }} diff --git a/client/app/bundles/course/material/folders/components/tables/TableMaterialRow.tsx b/client/app/bundles/course/material/folders/components/tables/TableMaterialRow.tsx index e4f735803c4..25a0f76cd4b 100644 --- a/client/app/bundles/course/material/folders/components/tables/TableMaterialRow.tsx +++ b/client/app/bundles/course/material/folders/components/tables/TableMaterialRow.tsx @@ -8,6 +8,7 @@ import Link from 'lib/components/core/Link'; import { getCourseId } from 'lib/helpers/url-helpers'; import { formatFullDateTime } from 'lib/moment'; +import KnowledgeBaseSwitch from '../buttons/KnowledgeBaseSwitch'; import WorkbinTableButtons from '../buttons/WorkbinTableButtons'; interface Props { @@ -15,10 +16,17 @@ interface Props { material: MaterialMiniEntity; isCurrentCourseStudent: boolean; isConcrete: boolean; + canManageKnowledgeBase: boolean; } const TableMaterialRow: FC = (props) => { - const { currFolderId, material, isCurrentCourseStudent, isConcrete } = props; + const { + currFolderId, + material, + isCurrentCourseStudent, + isConcrete, + canManageKnowledgeBase, + } = props; return ( @@ -80,6 +88,21 @@ const TableMaterialRow: FC = (props) => { - )} + {canManageKnowledgeBase && ( + + + + + + )} = (props) => { }`, }, }} + state={material.workflowState} type="material" /> diff --git a/client/app/bundles/course/material/folders/components/tables/TableSubfolderRow.tsx b/client/app/bundles/course/material/folders/components/tables/TableSubfolderRow.tsx index a98de079ed4..26f75e49d22 100644 --- a/client/app/bundles/course/material/folders/components/tables/TableSubfolderRow.tsx +++ b/client/app/bundles/course/material/folders/components/tables/TableSubfolderRow.tsx @@ -21,6 +21,7 @@ interface Props { subfolder: FolderMiniEntity; isCurrentCourseStudent: boolean; isConcrete: boolean; + canManageKnowledgeBase: boolean; } const translations = defineMessages({ @@ -37,7 +38,13 @@ const translations = defineMessages({ }); const TableSubfolderRow: FC = (props) => { - const { currFolderId, subfolder, isCurrentCourseStudent, isConcrete } = props; + const { + currFolderId, + subfolder, + isCurrentCourseStudent, + isConcrete, + canManageKnowledgeBase, + } = props; const { t } = useTranslation(); return ( @@ -52,7 +59,9 @@ const TableSubfolderRow: FC = (props) => { whiteSpace: 'normal', wordBreak: 'break-word', }} - to={`/courses/${getCourseId()}/materials/folders/${subfolder.id}`} + to={`/courses/${getCourseId()}/materials/folders/${ + subfolder.id + }/`} underline="hover" > {`${subfolder.name} (${subfolder.itemCount})`} @@ -113,6 +122,13 @@ const TableSubfolderRow: FC = (props) => { )} + {canManageKnowledgeBase && ( + + + - + + + )} = (props) => { isConcrete={isConcrete} itemId={subfolder.id} itemName={subfolder.name} + state={null} type="subfolder" /> diff --git a/client/app/bundles/course/material/folders/components/tables/WorkbinTable.tsx b/client/app/bundles/course/material/folders/components/tables/WorkbinTable.tsx index 270b603676a..29ec3589641 100644 --- a/client/app/bundles/course/material/folders/components/tables/WorkbinTable.tsx +++ b/client/app/bundles/course/material/folders/components/tables/WorkbinTable.tsx @@ -27,6 +27,7 @@ interface Props extends WrappedComponentProps { subfolders: FolderMiniEntity[]; materials: MaterialMiniEntity[]; isCurrentCourseStudent: boolean; + canManageKnowledgeBase: boolean; isConcrete: boolean; } @@ -36,6 +37,7 @@ const WorkbinTable: FC = (props) => { subfolders, materials, isCurrentCourseStudent, + canManageKnowledgeBase, isConcrete, } = props; @@ -126,6 +128,18 @@ const WorkbinTable: FC = (props) => { ); }; + const columnHeaderWithoutSort = (columnName: string): JSX.Element => { + return ( + + ); + }; + return ( @@ -135,7 +149,9 @@ const WorkbinTable: FC = (props) => { {!isCurrentCourseStudent && ( {columnHeaderWithSort('Start At')} )} - + {canManageKnowledgeBase && ( + {columnHeaderWithoutSort('Knowledge Base')} + )} @@ -143,6 +159,7 @@ const WorkbinTable: FC = (props) => { return ( = (props) => { return ( { const payload = new FormData(); @@ -181,6 +182,61 @@ export function deleteMaterial( }); } +export function removeChunks( + currFolderId: number, + materialId: number, +): Operation { + return async (dispatch) => + CourseAPI.folders + .deleteMaterialChunks(currFolderId, materialId) + .then(() => { + dispatch( + actions.updateMaterialWorkflowStateList(materialId, 'not_chunked'), + ); + }); +} + +export function chunkMaterial( + currFolderId: number, + materialId: number, + handleSuccess: () => void, + handleFailure: () => void, +): Operation { + return async (dispatch) => { + // Dispatch initial update to set workflow state to 'chunking' + dispatch(actions.updateMaterialWorkflowStateList(materialId, 'chunking')); + + // Make the API call to start chunking the material + CourseAPI.folders + .chunkMaterial(currFolderId, materialId) + .then((response) => { + const jobUrl = response.data.jobUrl; + pollJob( + jobUrl, + () => { + // Dispatch update to set workflow state to 'chunked' when job completes + dispatch( + actions.updateMaterialWorkflowStateList(materialId, 'chunked'), + ); + handleSuccess(); + }, + () => { + // Dispatch update to set workflow state to 'not chunked' when job errors + dispatch( + actions.updateMaterialWorkflowStateList( + materialId, + 'not_chunked', + ), + ); + handleFailure(); + }, + CHUNK_MATERIAL_JOB_POLL_INTERVAL_MS, + ); + }) + .catch(handleFailure); + }; +} + export function updateMaterial( formData: MaterialFormData, folderId: number, diff --git a/client/app/bundles/course/material/folders/pages/FolderShow/index.tsx b/client/app/bundles/course/material/folders/pages/FolderShow/index.tsx index 216151471ae..3f0124198d4 100644 --- a/client/app/bundles/course/material/folders/pages/FolderShow/index.tsx +++ b/client/app/bundles/course/material/folders/pages/FolderShow/index.tsx @@ -134,6 +134,7 @@ const FolderShow: FC = () => { > { break; } + case UPDATE_MATERIAL_WORKFLOW_STATE_LIST: { + const materialId = action.materialId; + const material = draft.materials.byId[materialId]; + if (material) { + material.workflowState = action.state; + saveListToStore(draft.materials, [material]); + } + break; + } + case DELETE_MATERIAL_LIST: { const materialId = action.materialId; if (draft.materials.byId[materialId]) { @@ -135,6 +148,12 @@ export const actions = { ): SaveMaterialListAction => { return { type: SAVE_MATERIAL_LIST, materialList }; }, + updateMaterialWorkflowStateList: ( + materialId: number, + state: 'not_chunked' | 'chunking' | 'chunked', + ): UpdateMaterialWorkflowStateAction => { + return { type: UPDATE_MATERIAL_WORKFLOW_STATE_LIST, materialId, state }; + }, }; export default reducer; diff --git a/client/app/bundles/course/material/folders/types.ts b/client/app/bundles/course/material/folders/types.ts index f8309731629..a1f8132ad3e 100644 --- a/client/app/bundles/course/material/folders/types.ts +++ b/client/app/bundles/course/material/folders/types.ts @@ -1,3 +1,4 @@ +import { number, string } from 'prop-types'; import { FolderListData, FolderMiniEntity, @@ -13,6 +14,8 @@ export const DELETE_FOLDER_LIST = 'course/materials/folders/DELETE_FOLDER_LIST'; export const DELETE_MATERIAL_LIST = 'course/materials/folders/DELETE_MATERIAL_LIST'; export const SAVE_MATERIAL_LIST = 'course/materials/folders/SAVE_MATERIAL_LIST'; +export const UPDATE_MATERIAL_WORKFLOW_STATE_LIST = + 'course/materials/folders/UPDATE_MATERIAL_WORKFLOW_STATE_LIST'; // Action Types export interface SaveFolderAction { @@ -46,11 +49,18 @@ export interface DeleteMaterialListAction { materialId: number; } +export interface UpdateMaterialWorkflowStateAction { + type: typeof UPDATE_MATERIAL_WORKFLOW_STATE_LIST; + materialId: number; + state: 'not_chunked' | 'chunking' | 'chunked' | null; +} + export type FoldersActionType = | SaveFolderAction | DeleteFolderListAction | DeleteMaterialListAction - | SaveMaterialListAction; + | SaveMaterialListAction + | UpdateMaterialWorkflowStateAction; // State Types export interface FoldersState { diff --git a/client/app/types/course/material/folders.ts b/client/app/types/course/material/folders.ts index 7bacfdfad25..9406071fa19 100644 --- a/client/app/types/course/material/folders.ts +++ b/client/app/types/course/material/folders.ts @@ -7,6 +7,7 @@ import { // Permissions for rendering title bar buttons export type FolderPermissions = Permissions< + | 'canManageKnowledgeBase' | 'isCurrentCourseStudent' | 'canStudentUpload' | 'canCreateSubfolder' @@ -15,7 +16,11 @@ export type FolderPermissions = Permissions< >; export type SubfolderPermissions = Permissions< - 'canStudentUpload' | 'showSdlWarning' | 'canEdit' | 'canDelete' + | 'canStudentUpload' + | 'showSdlWarning' + | 'canEdit' + | 'canDelete' + | 'canManageKnowledgeBase' >; export type MaterialPermissions = Permissions<'canEdit' | 'canDelete'>; @@ -41,6 +46,7 @@ export interface MaterialListData { updatedAt: string; updater: CourseUserBasicListData; permissions: MaterialPermissions; + workflowState: 'not_chunked' | 'chunking' | 'chunked' | null; } export interface FolderMiniEntity { @@ -63,6 +69,7 @@ export interface MaterialMiniEntity { updatedAt: string; updater: CourseUserBasicMiniEntity; permissions: MaterialPermissions; + workflowState: 'not_chunked' | 'chunking' | 'chunked' | null; } export interface FolderData { @@ -74,6 +81,7 @@ export interface FolderData { isConcrete: boolean; startAt: string; endAt: string | null; + workflowState: 'not_chunked' | 'chunking' | 'chunked' | null; }; subfolders: FolderListData[]; materials: MaterialListData[];