Skip to content

Commit d79e69c

Browse files
committed
chore: base modifications for restore action
1 parent fbf9358 commit d79e69c

8 files changed

Lines changed: 325 additions & 2 deletions

File tree

src/library-authoring/create-library/CreateLibrary.tsx

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
1016
import { Formik } from 'formik';
1117
import { useNavigate } from 'react-router-dom';
1218
import * as Yup from 'yup';
1319
import classNames from 'classnames';
20+
import { useState, useCallback } from 'react';
1421

1522
import { REGEX_RULES } from '@src/constants';
1623
import { useOrganizationListData } from '@src/generic/data/apiHooks';
@@ -22,6 +29,9 @@ import FormikErrorFeedback from '@src/generic/FormikErrorFeedback';
2229
import AlertError from '@src/generic/alert-error';
2330

2431
import { useCreateLibraryV2 } from './data/apiHooks';
32+
import { CreateContentLibraryArgs } from './data/api';
33+
import { useCreateLibraryRestore, useGetLibraryRestoreStatus } from './data/restoreHooks';
34+
import { LibraryRestoreStatus } from './data/restoreConstants';
2535
import messages from './messages';
2636
import type { ContentLibrary } from '../data/api';
2737

@@ -47,6 +57,11 @@ export const CreateLibrary = ({
4757
const { noSpaceRule, specialCharsRule } = REGEX_RULES;
4858
const validSlugIdRegex = /^[a-zA-Z\d]+(?:[\w-]*[a-zA-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
</>

src/library-authoring/create-library/data/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface CreateContentLibraryArgs {
1414
title: string,
1515
org: string,
1616
slug: string,
17+
learning_package?: number,
1718
}
1819

1920
/**
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
2+
import { CreateLibraryRestoreResponse, GetLibraryRestoreStatusResponse } from './restoreConstants';
3+
import { getLibraryRestoreApiUrl, getLibraryRestoreStatusApiUrl } from '../../data/api';
4+
5+
export const createLibraryRestore = async (archiveFile: File): Promise<CreateLibraryRestoreResponse> => {
6+
const formData = new FormData();
7+
formData.append('file', archiveFile);
8+
9+
const { data } = await getAuthenticatedHttpClient().post(getLibraryRestoreApiUrl(), formData, {
10+
headers: {
11+
'Content-Type': 'multipart/form-data',
12+
},
13+
});
14+
return data;
15+
};
16+
17+
export const getLibraryRestoreStatus = async (taskId: string): Promise<GetLibraryRestoreStatusResponse> => {
18+
const { data } = await getAuthenticatedHttpClient().get(getLibraryRestoreStatusApiUrl(taskId));
19+
return data;
20+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export interface CreateLibraryRestoreResponse {
2+
task_id: string;
3+
}
4+
5+
export interface LibraryRestoreResult {
6+
learning_package_id: number;
7+
title: string;
8+
org: string;
9+
slug: string;
10+
key: string;
11+
archive_key: string;
12+
containers: number;
13+
components: number;
14+
collections: number;
15+
sections: number;
16+
subsections: number;
17+
units: number;
18+
created_on_server: string;
19+
created_at: string;
20+
created_by: {
21+
username: string;
22+
email: string;
23+
};
24+
}
25+
26+
export interface GetLibraryRestoreStatusResponse {
27+
state: LibraryRestoreStatus;
28+
result: LibraryRestoreResult | null;
29+
error: string | null;
30+
error_log: string | null;
31+
}
32+
33+
export enum LibraryRestoreStatus {
34+
Pending = 'Pending',
35+
Succeeded = 'Succeeded',
36+
Failed = 'Failed',
37+
}
38+
39+
export const libraryRestoreQueryKeys = {
40+
all: ['library-v2-restore'],
41+
restoreStatus: (taskId: string) => [...libraryRestoreQueryKeys.all, 'status', taskId],
42+
restoreMutation: () => [...libraryRestoreQueryKeys.all, 'create-restore'],
43+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useMutation, useQuery } from '@tanstack/react-query';
2+
import { createLibraryRestore, getLibraryRestoreStatus } from './restoreApi';
3+
import {
4+
CreateLibraryRestoreResponse,
5+
GetLibraryRestoreStatusResponse,
6+
libraryRestoreQueryKeys,
7+
LibraryRestoreStatus,
8+
} from './restoreConstants';
9+
10+
/**
11+
* React Query hook to fetch restore status for a specific task
12+
*
13+
* @param taskId - The unique identifier of the restore task
14+
*
15+
* @example
16+
* ```tsx
17+
* const { data, isLoading, isError } = useGetLibraryRestoreStatus('task:456abc');
18+
* ```
19+
*/
20+
export const useGetLibraryRestoreStatus = (taskId: string) => useQuery<GetLibraryRestoreStatusResponse, Error>({
21+
queryKey: libraryRestoreQueryKeys.restoreStatus(taskId),
22+
queryFn: () => getLibraryRestoreStatus(taskId),
23+
enabled: !!taskId, // Only run the query if taskId is provided
24+
refetchInterval: (query) => (query.state.data?.state === LibraryRestoreStatus.Pending ? 2000 : false),
25+
});
26+
27+
export const useCreateLibraryRestore = () => useMutation<CreateLibraryRestoreResponse, Error, File>({
28+
mutationKey: libraryRestoreQueryKeys.restoreMutation(),
29+
mutationFn: createLibraryRestore,
30+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export { CreateLibrary } from './CreateLibrary';
22
export { CreateLibraryModal } from './CreateLibraryModal';
3+
export { useCreateLibraryRestore, useGetLibraryRestoreStatus } from './data/restoreHooks';
4+
export { LibraryRestoreStatus } from './data/restoreConstants';
5+
export type { LibraryRestoreResult, GetLibraryRestoreStatusResponse } from './data/restoreConstants';

0 commit comments

Comments
 (0)