Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/webpack-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const jsWebpackConfig: Configuration = {
entry: {
edit: { import: `${SOURCE_DIR}/edit.tsx`, dependOn: 'editor' },
editor: `${SOURCE_DIR}/editor.ts`,
import: `${SOURCE_DIR}/import.tsx`,
manage: `${SOURCE_DIR}/manage.ts`,
mce: `${SOURCE_DIR}/mce.ts`,
prism: `${SOURCE_DIR}/prism.ts`,
Expand Down
201 changes: 201 additions & 0 deletions src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import React, { useState, useRef, useEffect } from 'react'
import { __ } from '@wordpress/i18n'
import { Button } from '../../common/Button'
import {
DuplicateActionSelector,
DragDropUploadArea,
SelectedFilesList,
SnippetSelectionTable,
ImportResultDisplay
} from './components'
import { ImportCard } from '../shared'
import {
useFileSelection,
useSnippetSelection,
useImportWorkflow
} from './hooks'

type DuplicateAction = 'ignore' | 'replace' | 'skip'
type Step = 'upload' | 'select'

export const FileUploadForm: React.FC = () => {
const [duplicateAction, setDuplicateAction] = useState<DuplicateAction>('ignore')
const [currentStep, setCurrentStep] = useState<Step>('upload')
const selectSectionRef = useRef<HTMLDivElement>(null)

const fileSelection = useFileSelection()
const importWorkflow = useImportWorkflow()
const snippetSelection = useSnippetSelection(importWorkflow.availableSnippets)

useEffect(() => {
if (currentStep === 'select' && selectSectionRef.current) {
selectSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}, [currentStep])

const handleFileSelect = (files: FileList | null) => {
fileSelection.handleFileSelect(files)
importWorkflow.clearUploadResult()
}

const handleParseFiles = async () => {
if (!fileSelection.selectedFiles) return

const success = await importWorkflow.parseFiles(fileSelection.selectedFiles)
if (success) {
snippetSelection.clearSelection()
setCurrentStep('select')
}
}

const handleImportSelected = async () => {
const snippetsToImport = snippetSelection.getSelectedSnippets()
await importWorkflow.importSnippets(snippetsToImport, duplicateAction)
}

const handleBackToUpload = () => {
setCurrentStep('upload')
fileSelection.clearFiles()
snippetSelection.clearSelection()
importWorkflow.resetWorkflow()
}

const isUploadDisabled = !fileSelection.selectedFiles ||
fileSelection.selectedFiles.length === 0 ||
importWorkflow.isUploading

const isImportDisabled = snippetSelection.selectedSnippets.size === 0 ||
importWorkflow.isImporting

return (
<div className="wrap">
<div className="import-form-container" style={{ maxWidth: '800px' }}>
<p>{__('Upload one or more Code Snippets export files and the snippets will be imported.', 'code-snippets')}</p>

<p>
{__('Afterward, you will need to visit the ', 'code-snippets')}
<a href="admin.php?page=snippets">
{__('All Snippets', 'code-snippets')}
</a>
{__(' page to activate the imported snippets.', 'code-snippets')}
</p>

{currentStep === 'upload' && (
<>

{(!importWorkflow.uploadResult || !importWorkflow.uploadResult.success) && (
<>
<DuplicateActionSelector
value={duplicateAction}
onChange={setDuplicateAction}
/>

<ImportCard>
<h2 style={{ margin: '0 0 1em 0' }}>{__('Choose Files', 'code-snippets')}</h2>
<p className="description" style={{ marginBottom: '1em' }}>
{__('Choose one or more Code Snippets (.xml or .json) files to parse and preview.', 'code-snippets')}
</p>

<DragDropUploadArea
fileInputRef={fileSelection.fileInputRef}
onFileSelect={handleFileSelect}
disabled={importWorkflow.isUploading}
/>

{fileSelection.selectedFiles && fileSelection.selectedFiles.length > 0 && (
<SelectedFilesList
files={fileSelection.selectedFiles}
onRemoveFile={fileSelection.removeFile}
/>
)}

<div style={{ textAlign: 'center' }}>
<Button
primary
onClick={handleParseFiles}
disabled={isUploadDisabled}
style={{ minWidth: '200px' }}
>
{importWorkflow.isUploading
? __('Uploading files...', 'code-snippets')
: __('Upload files', 'code-snippets')
}
</Button>
</div>
</ImportCard>
</>
)}
</>
)}

{currentStep === 'select' && importWorkflow.availableSnippets.length > 0 && !importWorkflow.uploadResult?.success && (
<ImportCard ref={selectSectionRef}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px' }}>
<Button onClick={handleBackToUpload} className="button-link">
{__('← Upload Different Files', 'code-snippets')}
</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<div>
<h3 style={{ margin: '0' }}>{__('Available Snippets', 'code-snippets')} ({importWorkflow.availableSnippets.length})</h3>
<p style={{ margin: '0.5em 0 1em 0', color: '#666' }}>
{__('Select the snippets you want to import:', 'code-snippets')}
</p>
</div>
<div>
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={handleImportSelected}
disabled={isImportDisabled}
>
{importWorkflow.isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
</Button>
</div>
</div>

<SnippetSelectionTable
snippets={importWorkflow.availableSnippets}
selectedSnippets={snippetSelection.selectedSnippets}
isAllSelected={snippetSelection.isAllSelected}
onSnippetToggle={snippetSelection.handleSnippetToggle}
onSelectAll={snippetSelection.handleSelectAll}
/>

<div style={{ textAlign: 'end', marginTop: '1em' }}>
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={handleImportSelected}
disabled={isImportDisabled}
>
{importWorkflow.isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
</Button>
</div>
</ImportCard>
)}

{importWorkflow.uploadResult && (
<ImportResultDisplay result={importWorkflow.uploadResult} />
)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useDragAndDrop } from '../hooks/useDragAndDrop'

interface DragDropUploadAreaProps {
fileInputRef: React.RefObject<HTMLInputElement>
onFileSelect: (files: FileList | null) => void
disabled?: boolean
}

export const DragDropUploadArea: React.FC<DragDropUploadAreaProps> = ({
fileInputRef,
onFileSelect,
disabled = false
}) => {
const { dragOver, handleDragOver, handleDragLeave, handleDrop } = useDragAndDrop({
onFilesDrop: onFileSelect
})

const handleClick = () => {
if (!disabled) {
fileInputRef.current?.click()
}
}

return (
<>
<div
className={`upload-drop-zone ${dragOver ? 'drag-over' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleClick}
style={{
border: `2px dashed ${dragOver ? '#0073aa' : '#ccd0d4'}`,
borderRadius: '4px',
padding: '40px 20px',
textAlign: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: dragOver ? '#f0f6fc' : disabled ? '#f6f7f7' : '#fafafa',
marginBottom: '20px',
transition: 'all 0.3s ease',
opacity: disabled ? 0.6 : 1
}}
>
<div style={{ fontSize: '48px', marginBottom: '10px', color: '#666' }}>📁</div>
<p style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '500' }}>
{__('Drag and drop files here, or click to browse', 'code-snippets')}
</p>
<p style={{ margin: '0', color: '#666', fontSize: '14px' }}>
{__('Supports JSON and XML files', 'code-snippets')}
</p>
</div>

<input
ref={fileInputRef}
type="file"
accept="application/json,.json,text/xml"
multiple
onChange={(e) => onFileSelect(e.target.files)}
style={{ display: 'none' }}
disabled={disabled}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { ImportCard } from '../../shared'

type DuplicateAction = 'ignore' | 'replace' | 'skip'

interface DuplicateActionSelectorProps {
value: DuplicateAction
onChange: (action: DuplicateAction) => void
}

export const DuplicateActionSelector: React.FC<DuplicateActionSelectorProps> = ({
value,
onChange
}) => {
return (
<ImportCard>
<h2 style={{ margin: '0 0 1em 0' }}>{__('Duplicate Snippets', 'code-snippets')}</h2>
<p className="description" style={{ marginBottom: '1em' }}>
{__('What should happen if an existing snippet is found with an identical name to an imported snippet?', 'code-snippets')}
</p>

<fieldset>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="ignore"
checked={value === 'ignore'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Ignore any duplicate snippets: import all snippets from the file regardless and leave all existing snippets unchanged.', 'code-snippets')}
</span>
</label>

<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="replace"
checked={value === 'replace'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Replace any existing snippets with a newly imported snippet of the same name.', 'code-snippets')}
</span>
</label>

<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="skip"
checked={value === 'skip'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Do not import any duplicate snippets; leave all existing snippets unchanged.', 'code-snippets')}
</span>
</label>
</div>
</fieldset>
</ImportCard>
)
}
Loading
Loading