@@ -6,11 +6,18 @@ import {
66 Button ,
77 StatefulButton ,
88 ActionRow ,
9+ Dropzone ,
10+ Card ,
11+ Stack ,
12+ Icon ,
13+ Alert ,
914} from '@openedx/paragon' ;
15+ import { Upload , InsertDriveFile , CheckCircle } from '@openedx/paragon/icons' ;
1016import { Formik } from 'formik' ;
1117import { useNavigate } from 'react-router-dom' ;
1218import * as Yup from 'yup' ;
1319import classNames from 'classnames' ;
20+ import { useState , useCallback } from 'react' ;
1421
1522import { REGEX_RULES } from '@src/constants' ;
1623import { useOrganizationListData } from '@src/generic/data/apiHooks' ;
@@ -22,6 +29,9 @@ import FormikErrorFeedback from '@src/generic/FormikErrorFeedback';
2229import AlertError from '@src/generic/alert-error' ;
2330
2431import { useCreateLibraryV2 } from './data/apiHooks' ;
32+ import { CreateContentLibraryArgs } from './data/api' ;
33+ import { useCreateLibraryRestore , useGetLibraryRestoreStatus } from './data/restoreHooks' ;
34+ import { LibraryRestoreStatus } from './data/restoreConstants' ;
2535import messages from './messages' ;
2636import type { ContentLibrary } from '../data/api' ;
2737
@@ -47,6 +57,11 @@ export const CreateLibrary = ({
4757 const { noSpaceRule, specialCharsRule } = REGEX_RULES ;
4858 const validSlugIdRegex = / ^ [ a - z A - Z \d ] + (?: [ \w - ] * [ a - z A - Z \d ] + ) * $ / ;
4959
60+ // State for archive creation
61+ const [ isFromArchive , setIsFromArchive ] = useState ( false ) ;
62+ const [ uploadedFile , setUploadedFile ] = useState < File | null > ( null ) ;
63+ const [ restoreTaskId , setRestoreTaskId ] = useState < string > ( '' ) ;
64+
5065 const {
5166 mutate,
5267 data,
@@ -55,6 +70,11 @@ export const CreateLibrary = ({
5570 error,
5671 } = useCreateLibraryV2 ( ) ;
5772
73+ const restoreMutation = useCreateLibraryRestore ( ) ;
74+ const {
75+ data : restoreStatus ,
76+ } = useGetLibraryRestoreStatus ( restoreTaskId ) ;
77+
5878 const {
5979 data : allOrganizations ,
6080 isLoading : isOrganizationListLoading ,
@@ -81,6 +101,45 @@ export const CreateLibrary = ({
81101 }
82102 } ;
83103
104+ // Handle toggling create from archive mode
105+ const handleCreateFromArchive = useCallback ( ( ) => {
106+ setIsFromArchive ( true ) ;
107+ } , [ ] ) ;
108+
109+ // Handle file upload
110+ const handleFileUpload = useCallback ( ( {
111+ fileData,
112+ handleError,
113+ } : {
114+ fileData : FormData ;
115+ requestConfig : any ;
116+ handleError : any ;
117+ } ) => {
118+ const file = fileData . get ( 'file' ) as File ;
119+ if ( file ) {
120+ // Validate file type
121+ const validExtensions = [ '.zip' , '.tar.gz' , '.tar' ] ;
122+ const fileName = file . name . toLowerCase ( ) ;
123+ const isValidFile = validExtensions . some ( ext => fileName . endsWith ( ext ) ) ;
124+
125+ if ( isValidFile ) {
126+ setUploadedFile ( file ) ;
127+ // Immediately start the restore process
128+ restoreMutation . mutate ( file , {
129+ onSuccess : ( response ) => {
130+ setRestoreTaskId ( response . task_id ) ;
131+ } ,
132+ onError : ( restoreError ) => {
133+ handleError ( restoreError ) ;
134+ } ,
135+ } ) ;
136+ } else {
137+ // Call handleError for invalid file types
138+ handleError ( new Error ( 'Invalid file type. Please upload a .zip, .tar.gz, or .tar file.' ) ) ;
139+ }
140+ }
141+ } , [ restoreMutation ] ) ;
142+
84143 if ( data ) {
85144 if ( handlePostCreate ) {
86145 handlePostCreate ( data ) ;
@@ -96,8 +155,116 @@ export const CreateLibrary = ({
96155 { ! showInModal && (
97156 < SubHeader
98157 title = { intl . formatMessage ( messages . createLibrary ) }
158+ headerActions = { ! isFromArchive ? (
159+ < Button
160+ variant = "outline-primary"
161+ onClick = { handleCreateFromArchive }
162+ >
163+ { intl . formatMessage ( messages . createFromArchiveButton ) }
164+ </ Button >
165+ ) : null }
99166 />
100167 ) }
168+
169+ { /* Archive upload section - shown above form when in archive mode */ }
170+ { isFromArchive && (
171+ < div className = "mb-4" >
172+ { ! uploadedFile && (
173+ < Dropzone
174+ data-testid = "library-archive-dropzone"
175+ accept = { {
176+ 'application/zip' : [ '.zip' ] ,
177+ 'application/gzip' : [ '.tar.gz' ] ,
178+ 'application/x-tar' : [ '.tar' ] ,
179+ } }
180+ onProcessUpload = { handleFileUpload }
181+ maxSize = { 5 * 1024 * 1024 * 1024 } // 5GB
182+ style = { { height : '300px' } }
183+ errorMessages = { {
184+ invalidSize : intl . formatMessage ( messages . dropzoneSubtitle ) ,
185+ multipleDragged : 'Please upload only one archive file.' ,
186+ } }
187+ >
188+ < Stack direction = "vertical" gap = { 3 } className = "text-center" >
189+ < Icon src = { Upload } style = { { height : '64px' , width : '64px' } } />
190+ < div >
191+ < h4 > { intl . formatMessage ( messages . dropzoneTitle ) } </ h4 >
192+ < p className = "text-muted" > { intl . formatMessage ( messages . dropzoneSubtitle ) } </ p >
193+ </ div >
194+ </ Stack >
195+ </ Dropzone >
196+ ) }
197+
198+ { uploadedFile && restoreStatus ?. state === LibraryRestoreStatus . Succeeded && restoreStatus . result && (
199+ // Show restore result data when succeeded
200+ < Card className = "mb-4" >
201+ < Card . Body >
202+ < Stack direction = "horizontal" gap = { 3 } className = "align-items-center" >
203+ < Icon src = { CheckCircle } style = { { height : '40px' , width : '40px' , color : 'green' } } />
204+ < div className = "flex-grow-1" >
205+ < h5 className = "mb-1" > { restoreStatus . result . title } </ h5 >
206+ < p className = "text-muted mb-1" >
207+ { restoreStatus . result . org } / { restoreStatus . result . slug }
208+ </ p >
209+ < p className = "text-muted mb-0 small" >
210+ Contains { restoreStatus . result . components } Components •
211+ Backed up { new Date ( restoreStatus . result . created_at ) . toLocaleDateString ( ) } at{ ' ' }
212+ { new Date ( restoreStatus . result . created_at ) . toLocaleTimeString ( ) }
213+ </ p >
214+ </ div >
215+ </ Stack >
216+ </ Card . Body >
217+ </ Card >
218+ ) }
219+
220+ { uploadedFile && restoreStatus ?. state !== LibraryRestoreStatus . Succeeded && (
221+ // Show uploaded file info during processing
222+ < Card className = "mb-4" >
223+ < Card . Body >
224+ < Stack direction = "horizontal" gap = { 3 } className = "align-items-center" >
225+ < Icon src = { InsertDriveFile } style = { { height : '40px' , width : '40px' } } />
226+ < div className = "flex-grow-1" >
227+ < h5 className = "mb-1" > { uploadedFile . name } </ h5 >
228+ < p className = "text-muted mb-0" >
229+ { ( uploadedFile . size / ( 1024 * 1024 ) ) . toFixed ( 2 ) } MB
230+ </ p >
231+ </ div >
232+ { restoreMutation . isPending && (
233+ < div className = "spinner-border spinner-border-sm text-primary" role = "status" >
234+ < span className = "sr-only" > Processing...</ span >
235+ </ div >
236+ ) }
237+ </ Stack >
238+ </ Card . Body >
239+ </ Card >
240+ ) }
241+
242+ { /* Archive restore status */ }
243+ { restoreTaskId && (
244+ < div className = "mb-4" >
245+ { restoreStatus ?. state === LibraryRestoreStatus . Pending && (
246+ < Alert variant = "info" >
247+ { intl . formatMessage ( messages . restoreInProgress ) }
248+ </ Alert >
249+ ) }
250+ { restoreStatus ?. state === LibraryRestoreStatus . Failed && (
251+ < Alert variant = "danger" >
252+ { intl . formatMessage ( messages . restoreError ) }
253+ { restoreStatus . error_log && (
254+ < div >
255+ < a href = { restoreStatus . error_log } target = "_blank" rel = "noopener noreferrer" >
256+ View error log
257+ </ a >
258+ </ div >
259+ ) }
260+ </ Alert >
261+ ) }
262+ </ div >
263+ ) }
264+ </ div >
265+ ) }
266+
267+ { /* Regular form - always shown */ }
101268 < Formik
102269 initialValues = { {
103270 title : '' ,
@@ -123,7 +290,16 @@ export const CreateLibrary = ({
123290 ) ,
124291 } )
125292 }
126- onSubmit = { ( values ) => mutate ( values ) }
293+ onSubmit = { ( values ) => {
294+ const submitData = { ...values } as CreateContentLibraryArgs ;
295+
296+ // If we're creating from archive and have a successful restore, include the learning_package_id
297+ if ( isFromArchive && restoreStatus ?. state === LibraryRestoreStatus . Succeeded && restoreStatus . result ) {
298+ submitData . learning_package = restoreStatus . result . learning_package_id ;
299+ }
300+
301+ mutate ( submitData ) ;
302+ } }
127303 >
128304 { ( formikProps ) => (
129305 < Form onSubmit = { formikProps . handleSubmit } >
@@ -196,7 +372,9 @@ export const CreateLibrary = ({
196372 </ Form >
197373 ) }
198374 </ Formik >
199- { isError && ( < AlertError error = { error } /> ) }
375+ { ( isError || restoreMutation . isError ) && (
376+ < AlertError error = { error || restoreMutation . error } />
377+ ) }
200378 </ Container >
201379 { ! showInModal && ( < StudioFooterSlot /> ) }
202380 </ >
0 commit comments