@package-uploader/ui 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,8 +11,6 @@ interface FileItem {
|
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type Step = 'drop' | 'structure' | 'options';
|
|
15
|
-
|
|
16
14
|
interface UploadModalProps {
|
|
17
15
|
folderId: number;
|
|
18
16
|
folderName: string;
|
|
@@ -28,90 +26,100 @@ export default function UploadModal({
|
|
|
28
26
|
}: UploadModalProps) {
|
|
29
27
|
const [files, setFiles] = useState<FileItem[]>([]);
|
|
30
28
|
const [uploading, setUploading] = useState(false);
|
|
31
|
-
const [step, setStep] = useState<Step>('drop');
|
|
32
29
|
|
|
33
|
-
// Course structure
|
|
30
|
+
// Course structure — on-demand, collapsible
|
|
34
31
|
const [courseStructure, setCourseStructure] = useState<CourseStructure | null>(null);
|
|
35
32
|
const [classMappings, setClassMappings] = useState<CourseClassMappings>({});
|
|
36
33
|
const [parsingStructure, setParsingStructure] = useState(false);
|
|
34
|
+
const [structurePanelOpen, setStructurePanelOpen] = useState(false);
|
|
35
|
+
const [parseFailed, setParseFailed] = useState(false);
|
|
37
36
|
|
|
38
37
|
// LMS Thin Pack options
|
|
39
|
-
const [createThinPack, setCreateThinPack] = useState(true);
|
|
38
|
+
const [createThinPack, setCreateThinPack] = useState(true);
|
|
40
39
|
const [thinPackName, setThinPackName] = useState('');
|
|
41
40
|
|
|
42
41
|
// Shared Link options
|
|
43
|
-
const [createSharedLink, setCreateSharedLink] = useState(false);
|
|
42
|
+
const [createSharedLink, setCreateSharedLink] = useState(false);
|
|
44
43
|
const [sharedLinkName, setSharedLinkName] = useState('');
|
|
45
44
|
|
|
46
|
-
// Skin option
|
|
45
|
+
// Skin option
|
|
47
46
|
const [skinName, setSkinName] = useState('');
|
|
48
47
|
|
|
49
|
-
// Wrap & Download mode
|
|
48
|
+
// Wrap & Download mode
|
|
50
49
|
const [wrapOnly, setWrapOnly] = useState(false);
|
|
51
50
|
|
|
52
|
-
// Generate slug from name: replace spaces with underscores
|
|
53
51
|
function generateSlug(name: string): string {
|
|
54
52
|
return name.trim().replace(/\s+/g, '_');
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
function handleFilesAccepted(newFiles: File[]) {
|
|
58
56
|
const fileItems: FileItem[] = newFiles.map((file) => ({
|
|
59
57
|
file,
|
|
60
58
|
status: 'pending',
|
|
61
59
|
}));
|
|
62
60
|
setFiles((prev) => [...prev, ...fileItems]);
|
|
63
61
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const structure = await api.parseCourseStructure(targetFile);
|
|
71
|
-
setCourseStructure(structure);
|
|
72
|
-
} catch {
|
|
73
|
-
setCourseStructure(null);
|
|
74
|
-
}
|
|
75
|
-
setParsingStructure(false);
|
|
76
|
-
} else {
|
|
77
|
-
// Multi-file: no structure parsing
|
|
78
|
-
setCourseStructure(null);
|
|
79
|
-
}
|
|
62
|
+
// Reset structure state when files change
|
|
63
|
+
setCourseStructure(null);
|
|
64
|
+
setClassMappings({});
|
|
65
|
+
setStructurePanelOpen(false);
|
|
66
|
+
setParseFailed(false);
|
|
80
67
|
}
|
|
81
68
|
|
|
82
69
|
function handleRemoveFile(index: number) {
|
|
83
70
|
setFiles((prev) => {
|
|
84
71
|
const next = prev.filter((_, i) => i !== index);
|
|
85
|
-
// Reset structure if we removed the only file or now have multiple
|
|
86
72
|
if (next.length !== 1) {
|
|
87
73
|
setCourseStructure(null);
|
|
88
74
|
setClassMappings({});
|
|
75
|
+
setStructurePanelOpen(false);
|
|
76
|
+
setParseFailed(false);
|
|
89
77
|
}
|
|
90
78
|
return next;
|
|
91
79
|
});
|
|
92
80
|
}
|
|
93
81
|
|
|
82
|
+
async function handleCustomizeClick() {
|
|
83
|
+
// Already parsed — just toggle
|
|
84
|
+
if (courseStructure) {
|
|
85
|
+
setStructurePanelOpen((prev) => !prev);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Already failed — don't retry
|
|
90
|
+
if (parseFailed) return;
|
|
91
|
+
|
|
92
|
+
// Parse on first click
|
|
93
|
+
if (files.length !== 1) return;
|
|
94
|
+
|
|
95
|
+
setParsingStructure(true);
|
|
96
|
+
try {
|
|
97
|
+
const structure = await api.parseCourseStructure(files[0].file);
|
|
98
|
+
if (structure) {
|
|
99
|
+
setCourseStructure(structure);
|
|
100
|
+
setStructurePanelOpen(true);
|
|
101
|
+
setParseFailed(false);
|
|
102
|
+
} else {
|
|
103
|
+
setParseFailed(true);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
setParseFailed(true);
|
|
107
|
+
}
|
|
108
|
+
setParsingStructure(false);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
function formatSize(bytes: number): string {
|
|
95
112
|
if (bytes < 1024) return `${bytes} B`;
|
|
96
113
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
114
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
function handleNextFromDrop() {
|
|
101
|
-
if (courseStructure && files.length === 1) {
|
|
102
|
-
setStep('structure');
|
|
103
|
-
} else {
|
|
104
|
-
setStep('options');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
117
|
async function handleUpload() {
|
|
109
118
|
const pendingFiles = files.filter((f) => f.status === 'pending');
|
|
110
119
|
if (pendingFiles.length === 0) return;
|
|
111
120
|
|
|
112
121
|
setUploading(true);
|
|
113
122
|
|
|
114
|
-
// Build class mappings option (only if any assignments exist)
|
|
115
123
|
const hasClassAssignments =
|
|
116
124
|
classMappings.course ||
|
|
117
125
|
Object.keys(classMappings.lessons || {}).length > 0 ||
|
|
@@ -120,14 +128,12 @@ export default function UploadModal({
|
|
|
120
128
|
for (let i = 0; i < files.length; i++) {
|
|
121
129
|
if (files[i].status !== 'pending') continue;
|
|
122
130
|
|
|
123
|
-
// Mark as uploading
|
|
124
131
|
setFiles((prev) =>
|
|
125
132
|
prev.map((f, idx) => (idx === i ? { ...f, status: 'uploading' } : f))
|
|
126
133
|
);
|
|
127
134
|
|
|
128
135
|
try {
|
|
129
136
|
if (wrapOnly) {
|
|
130
|
-
// Wrap & Download mode: wrap with PA-Patcher and download, no CDS upload
|
|
131
137
|
const wrapOptions: { skin?: string; classMappings?: CourseClassMappings } = {};
|
|
132
138
|
if (skinName.trim()) wrapOptions.skin = skinName.trim();
|
|
133
139
|
if (hasClassAssignments) wrapOptions.classMappings = classMappings;
|
|
@@ -143,7 +149,6 @@ export default function UploadModal({
|
|
|
143
149
|
)
|
|
144
150
|
);
|
|
145
151
|
} else {
|
|
146
|
-
// Normal upload mode
|
|
147
152
|
const uploadOptions: { title?: string; skin?: string; classMappings?: CourseClassMappings } = {};
|
|
148
153
|
if (skinName.trim()) uploadOptions.skin = skinName.trim();
|
|
149
154
|
if (hasClassAssignments) uploadOptions.classMappings = classMappings;
|
|
@@ -155,7 +160,6 @@ export default function UploadModal({
|
|
|
155
160
|
);
|
|
156
161
|
|
|
157
162
|
if (result.success && result.documentId) {
|
|
158
|
-
// Create LMS Thin Pack if enabled
|
|
159
163
|
if (createThinPack) {
|
|
160
164
|
const name = thinPackName.trim() || `${result.documentName} - LMS Thin Pack`;
|
|
161
165
|
try {
|
|
@@ -170,7 +174,6 @@ export default function UploadModal({
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
173
|
-
// Create Shared Link if enabled
|
|
174
177
|
if (createSharedLink) {
|
|
175
178
|
const name = sharedLinkName.trim() || `${result.documentName} - Private`;
|
|
176
179
|
try {
|
|
@@ -223,8 +226,8 @@ export default function UploadModal({
|
|
|
223
226
|
|
|
224
227
|
const pendingCount = files.filter((f) => f.status === 'pending').length;
|
|
225
228
|
const hasFiles = files.length > 0;
|
|
229
|
+
const isSingleFile = files.length === 1;
|
|
226
230
|
|
|
227
|
-
// Preview slugs
|
|
228
231
|
const thinPackSlugPreview = thinPackName.trim()
|
|
229
232
|
? generateSlug(thinPackName)
|
|
230
233
|
: '(will use document name)';
|
|
@@ -232,14 +235,27 @@ export default function UploadModal({
|
|
|
232
235
|
? generateSlug(sharedLinkName)
|
|
233
236
|
: '(will use document name)';
|
|
234
237
|
|
|
238
|
+
// Class assignment count for summary
|
|
239
|
+
const assignmentCount =
|
|
240
|
+
(classMappings.course ? 1 : 0) +
|
|
241
|
+
Object.keys(classMappings.lessons || {}).length +
|
|
242
|
+
Object.keys(classMappings.blocks || {}).length;
|
|
243
|
+
|
|
244
|
+
// Customize button label
|
|
245
|
+
const customizeLabel = parsingStructure
|
|
246
|
+
? 'Analyzing...'
|
|
247
|
+
: parseFailed
|
|
248
|
+
? 'Not a Rise course'
|
|
249
|
+
: courseStructure
|
|
250
|
+
? `Customize Classes (${courseStructure.lessons.length} lessons)`
|
|
251
|
+
: 'Customize Classes';
|
|
252
|
+
|
|
235
253
|
return (
|
|
236
254
|
<div className="modal-overlay" onClick={onClose}>
|
|
237
255
|
<div className="modal upload-modal" onClick={(e) => e.stopPropagation()}>
|
|
238
256
|
<div className="modal-header">
|
|
239
257
|
<h3 className="modal-title">
|
|
240
258
|
{wrapOnly ? 'Wrap & Download' : `Upload to: ${folderName}`}
|
|
241
|
-
{step === 'structure' && ' — Customize'}
|
|
242
|
-
{step === 'options' && ' — Options'}
|
|
243
259
|
</h3>
|
|
244
260
|
<button className="modal-close" onClick={onClose}>
|
|
245
261
|
x
|
|
@@ -247,264 +263,210 @@ export default function UploadModal({
|
|
|
247
263
|
</div>
|
|
248
264
|
|
|
249
265
|
<div className="upload-modal-content">
|
|
250
|
-
{/*
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
))}
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
|
|
294
|
-
{/* Parsing indicator */}
|
|
295
|
-
{parsingStructure && (
|
|
296
|
-
<div className="structure-parsing-indicator">
|
|
297
|
-
<div className="spinner" />
|
|
298
|
-
<span>Analyzing course structure...</span>
|
|
266
|
+
{/* Dropzone */}
|
|
267
|
+
<DropZone onFilesAccepted={handleFilesAccepted} disabled={uploading} />
|
|
268
|
+
|
|
269
|
+
{/* File List */}
|
|
270
|
+
{hasFiles && (
|
|
271
|
+
<div className="upload-file-list">
|
|
272
|
+
<h4>Files ({files.length})</h4>
|
|
273
|
+
{files.map((item, index) => (
|
|
274
|
+
<div key={index} className="upload-file-item">
|
|
275
|
+
<div className="upload-file-info">
|
|
276
|
+
<span className="upload-file-name">{item.file.name}</span>
|
|
277
|
+
<span className="upload-file-size">{formatSize(item.file.size)}</span>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="upload-file-status">
|
|
280
|
+
{item.status === 'pending' && (
|
|
281
|
+
<button
|
|
282
|
+
className="btn btn-sm btn-secondary"
|
|
283
|
+
onClick={() => handleRemoveFile(index)}
|
|
284
|
+
>
|
|
285
|
+
Remove
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
{item.status === 'uploading' && (
|
|
289
|
+
<>
|
|
290
|
+
<div className="spinner" />
|
|
291
|
+
<span className="status-uploading">{wrapOnly ? 'Wrapping...' : 'Uploading...'}</span>
|
|
292
|
+
</>
|
|
293
|
+
)}
|
|
294
|
+
{item.status === 'success' && (
|
|
295
|
+
<span className="status-success">{wrapOnly ? 'Downloaded' : 'Uploaded'}</span>
|
|
296
|
+
)}
|
|
297
|
+
{item.status === 'error' && (
|
|
298
|
+
<span className="status-error" title={item.error}>
|
|
299
|
+
Failed
|
|
300
|
+
</span>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
299
303
|
</div>
|
|
300
|
-
)}
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
{/* Customize Classes — on-demand toggle */}
|
|
309
|
+
{isSingleFile && !uploading && (
|
|
310
|
+
<div className="structure-section">
|
|
311
|
+
<button
|
|
312
|
+
className={`structure-toggle-btn${structurePanelOpen ? ' open' : ''}${parseFailed ? ' disabled' : ''}`}
|
|
313
|
+
onClick={handleCustomizeClick}
|
|
314
|
+
disabled={parsingStructure || parseFailed}
|
|
315
|
+
>
|
|
316
|
+
{parsingStructure && <div className="spinner" />}
|
|
317
|
+
<span>{customizeLabel}</span>
|
|
318
|
+
{!parsingStructure && !parseFailed && (
|
|
319
|
+
<span className="structure-toggle-arrow">
|
|
320
|
+
{structurePanelOpen ? '\u25B2' : '\u25BC'}
|
|
321
|
+
</span>
|
|
322
|
+
)}
|
|
323
|
+
{assignmentCount > 0 && !structurePanelOpen && (
|
|
324
|
+
<span className="course-structure-badge">{assignmentCount} assigned</span>
|
|
325
|
+
)}
|
|
326
|
+
</button>
|
|
310
327
|
|
|
311
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
{structurePanelOpen && courseStructure && (
|
|
329
|
+
<div className="structure-panel">
|
|
330
|
+
<CourseStructureStep
|
|
331
|
+
structure={courseStructure}
|
|
332
|
+
classMappings={classMappings}
|
|
333
|
+
onChange={setClassMappings}
|
|
334
|
+
/>
|
|
315
335
|
</div>
|
|
316
336
|
)}
|
|
317
|
-
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
{/* Step 2: Course Structure */}
|
|
321
|
-
{step === 'structure' && courseStructure && (
|
|
322
|
-
<CourseStructureStep
|
|
323
|
-
structure={courseStructure}
|
|
324
|
-
classMappings={classMappings}
|
|
325
|
-
onChange={setClassMappings}
|
|
326
|
-
/>
|
|
337
|
+
</div>
|
|
327
338
|
)}
|
|
328
339
|
|
|
329
|
-
{/*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* CDS upload options (hidden in wrap-only mode) */}
|
|
349
|
-
{!wrapOnly && (
|
|
350
|
-
<>
|
|
351
|
-
<h4>After upload, create:</h4>
|
|
340
|
+
{/* Options */}
|
|
341
|
+
<div className="upload-link-options">
|
|
342
|
+
{/* Wrap & Download toggle */}
|
|
343
|
+
<div className="upload-link-option">
|
|
344
|
+
<label className="upload-link-toggle">
|
|
345
|
+
<input
|
|
346
|
+
type="checkbox"
|
|
347
|
+
checked={wrapOnly}
|
|
348
|
+
onChange={(e) => setWrapOnly(e.target.checked)}
|
|
349
|
+
disabled={uploading}
|
|
350
|
+
/>
|
|
351
|
+
<span>Wrap & Download only (skip CDS upload)</span>
|
|
352
|
+
</label>
|
|
353
|
+
{wrapOnly && (
|
|
354
|
+
<span className="slug-preview">Wraps with PA-Patcher and downloads the ZIP directly</span>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
352
357
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
{/* CDS upload options */}
|
|
359
|
+
{!wrapOnly && (
|
|
360
|
+
<>
|
|
361
|
+
<h4>After upload, create:</h4>
|
|
362
|
+
|
|
363
|
+
<div className="upload-link-option">
|
|
364
|
+
<label className="upload-link-toggle">
|
|
365
|
+
<input
|
|
366
|
+
type="checkbox"
|
|
367
|
+
checked={createThinPack}
|
|
368
|
+
onChange={(e) => setCreateThinPack(e.target.checked)}
|
|
369
|
+
disabled={uploading}
|
|
370
|
+
/>
|
|
371
|
+
<span>LMS Thin Pack (public)</span>
|
|
372
|
+
</label>
|
|
373
|
+
{createThinPack && (
|
|
374
|
+
<div className="upload-link-name-input">
|
|
356
375
|
<input
|
|
357
|
-
type="
|
|
358
|
-
|
|
359
|
-
|
|
376
|
+
type="text"
|
|
377
|
+
placeholder="Link name (uses document name if empty)"
|
|
378
|
+
value={thinPackName}
|
|
379
|
+
onChange={(e) => setThinPackName(e.target.value)}
|
|
360
380
|
disabled={uploading}
|
|
361
381
|
/>
|
|
362
|
-
<span>
|
|
363
|
-
</
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<input
|
|
367
|
-
type="text"
|
|
368
|
-
placeholder="Link name (uses document name if empty)"
|
|
369
|
-
value={thinPackName}
|
|
370
|
-
onChange={(e) => setThinPackName(e.target.value)}
|
|
371
|
-
disabled={uploading}
|
|
372
|
-
/>
|
|
373
|
-
<span className="slug-preview">Slug: {thinPackSlugPreview}</span>
|
|
374
|
-
</div>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
382
|
+
<span className="slug-preview">Slug: {thinPackSlugPreview}</span>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
377
386
|
|
|
378
|
-
|
|
379
|
-
<
|
|
380
|
-
<
|
|
387
|
+
<div className="upload-link-option">
|
|
388
|
+
<label className="upload-link-toggle">
|
|
389
|
+
<input
|
|
390
|
+
type="checkbox"
|
|
391
|
+
checked={createSharedLink}
|
|
392
|
+
onChange={(e) => setCreateSharedLink(e.target.checked)}
|
|
393
|
+
disabled={uploading}
|
|
394
|
+
/>
|
|
395
|
+
<span>Shared Link (private/SSO)</span>
|
|
396
|
+
</label>
|
|
397
|
+
{createSharedLink && (
|
|
398
|
+
<div className="upload-link-name-input">
|
|
381
399
|
<input
|
|
382
|
-
type="
|
|
383
|
-
|
|
384
|
-
|
|
400
|
+
type="text"
|
|
401
|
+
placeholder="Link name (uses document name if empty)"
|
|
402
|
+
value={sharedLinkName}
|
|
403
|
+
onChange={(e) => setSharedLinkName(e.target.value)}
|
|
385
404
|
disabled={uploading}
|
|
386
405
|
/>
|
|
387
|
-
<span>
|
|
388
|
-
</
|
|
389
|
-
|
|
390
|
-
<div className="upload-link-name-input">
|
|
391
|
-
<input
|
|
392
|
-
type="text"
|
|
393
|
-
placeholder="Link name (uses document name if empty)"
|
|
394
|
-
value={sharedLinkName}
|
|
395
|
-
onChange={(e) => setSharedLinkName(e.target.value)}
|
|
396
|
-
disabled={uploading}
|
|
397
|
-
/>
|
|
398
|
-
<span className="slug-preview">Slug: {sharedLinkSlugPreview}</span>
|
|
399
|
-
</div>
|
|
400
|
-
)}
|
|
401
|
-
</div>
|
|
402
|
-
</>
|
|
403
|
-
)}
|
|
404
|
-
|
|
405
|
-
{/* Skin Option */}
|
|
406
|
-
<div className="upload-link-option" style={{ marginTop: '12px' }}>
|
|
407
|
-
<label className="upload-link-toggle">
|
|
408
|
-
<span>Skin (optional)</span>
|
|
409
|
-
</label>
|
|
410
|
-
<div className="upload-link-name-input">
|
|
411
|
-
<input
|
|
412
|
-
type="text"
|
|
413
|
-
placeholder="e.g. marketing"
|
|
414
|
-
value={skinName}
|
|
415
|
-
onChange={(e) => setSkinName(e.target.value)}
|
|
416
|
-
disabled={uploading}
|
|
417
|
-
/>
|
|
418
|
-
<span className="slug-preview">CSS class for custom skin styling</span>
|
|
406
|
+
<span className="slug-preview">Slug: {sharedLinkSlugPreview}</span>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
419
409
|
</div>
|
|
410
|
+
</>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
{/* Skin */}
|
|
414
|
+
<div className="upload-link-option" style={{ marginTop: '12px' }}>
|
|
415
|
+
<label className="upload-link-toggle">
|
|
416
|
+
<span>Skin (optional)</span>
|
|
417
|
+
</label>
|
|
418
|
+
<div className="upload-link-name-input">
|
|
419
|
+
<input
|
|
420
|
+
type="text"
|
|
421
|
+
placeholder="e.g. marketing"
|
|
422
|
+
value={skinName}
|
|
423
|
+
onChange={(e) => setSkinName(e.target.value)}
|
|
424
|
+
disabled={uploading}
|
|
425
|
+
/>
|
|
426
|
+
<span className="slug-preview">CSS class for custom skin styling</span>
|
|
420
427
|
</div>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
</div>
|
|
440
|
-
)}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Class assignments summary */}
|
|
432
|
+
{assignmentCount > 0 && !structurePanelOpen && (
|
|
433
|
+
<div className="structure-summary-badge">
|
|
434
|
+
Class assignments:{' '}
|
|
435
|
+
{[
|
|
436
|
+
classMappings.course ? '1 course' : '',
|
|
437
|
+
Object.keys(classMappings.lessons || {}).length > 0
|
|
438
|
+
? `${Object.keys(classMappings.lessons || {}).length} lesson(s)`
|
|
439
|
+
: '',
|
|
440
|
+
Object.keys(classMappings.blocks || {}).length > 0
|
|
441
|
+
? `${Object.keys(classMappings.blocks || {}).length} block(s)`
|
|
442
|
+
: '',
|
|
443
|
+
]
|
|
444
|
+
.filter(Boolean)
|
|
445
|
+
.join(', ')}
|
|
441
446
|
</div>
|
|
442
447
|
)}
|
|
443
448
|
</div>
|
|
444
449
|
|
|
445
450
|
<div className="modal-footer">
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
</button>
|
|
466
|
-
<button
|
|
467
|
-
className="btn btn-secondary"
|
|
468
|
-
onClick={() => {
|
|
469
|
-
setClassMappings({});
|
|
470
|
-
setStep('options');
|
|
471
|
-
}}
|
|
472
|
-
>
|
|
473
|
-
Skip
|
|
474
|
-
</button>
|
|
475
|
-
<button className="btn btn-primary" onClick={() => setStep('options')}>
|
|
476
|
-
Next
|
|
477
|
-
</button>
|
|
478
|
-
</>
|
|
479
|
-
)}
|
|
480
|
-
|
|
481
|
-
{step === 'options' && (
|
|
482
|
-
<>
|
|
483
|
-
<button
|
|
484
|
-
className="btn btn-secondary"
|
|
485
|
-
onClick={() => setStep(courseStructure && files.length === 1 ? 'structure' : 'drop')}
|
|
486
|
-
disabled={uploading}
|
|
487
|
-
>
|
|
488
|
-
Back
|
|
489
|
-
</button>
|
|
490
|
-
<button
|
|
491
|
-
className="btn btn-primary"
|
|
492
|
-
onClick={handleUpload}
|
|
493
|
-
disabled={uploading || pendingCount === 0}
|
|
494
|
-
>
|
|
495
|
-
{uploading ? (
|
|
496
|
-
<>
|
|
497
|
-
<div className="spinner" />
|
|
498
|
-
{wrapOnly ? 'Wrapping...' : 'Uploading...'}
|
|
499
|
-
</>
|
|
500
|
-
) : wrapOnly ? (
|
|
501
|
-
`Wrap & Download ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
502
|
-
) : (
|
|
503
|
-
`Upload ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
504
|
-
)}
|
|
505
|
-
</button>
|
|
506
|
-
</>
|
|
507
|
-
)}
|
|
451
|
+
<button className="btn btn-secondary" onClick={onClose} disabled={uploading}>
|
|
452
|
+
Cancel
|
|
453
|
+
</button>
|
|
454
|
+
<button
|
|
455
|
+
className="btn btn-primary"
|
|
456
|
+
onClick={handleUpload}
|
|
457
|
+
disabled={uploading || pendingCount === 0}
|
|
458
|
+
>
|
|
459
|
+
{uploading ? (
|
|
460
|
+
<>
|
|
461
|
+
<div className="spinner" />
|
|
462
|
+
{wrapOnly ? 'Wrapping...' : 'Uploading...'}
|
|
463
|
+
</>
|
|
464
|
+
) : wrapOnly ? (
|
|
465
|
+
`Wrap & Download ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
466
|
+
) : (
|
|
467
|
+
`Upload ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
468
|
+
)}
|
|
469
|
+
</button>
|
|
508
470
|
</div>
|
|
509
471
|
</div>
|
|
510
472
|
</div>
|