Skip to content

Commit c160afe

Browse files
committed
feat: import snippets
1 parent 6990cb6 commit c160afe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2798
-188
lines changed

config/webpack-js.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const jsWebpackConfig: Configuration = {
2626
entry: {
2727
edit: { import: `${SOURCE_DIR}/edit.tsx`, dependOn: 'editor' },
2828
editor: `${SOURCE_DIR}/editor.ts`,
29+
import: `${SOURCE_DIR}/import.tsx`,
2930
manage: `${SOURCE_DIR}/manage.ts`,
3031
mce: `${SOURCE_DIR}/mce.ts`,
3132
prism: `${SOURCE_DIR}/prism.ts`,
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React, { useState, useRef, useEffect } from 'react'
2+
import { __ } from '@wordpress/i18n'
3+
import { Button } from '../../common/Button'
4+
import {
5+
DuplicateActionSelector,
6+
DragDropUploadArea,
7+
SelectedFilesList,
8+
SnippetSelectionTable,
9+
ImportResultDisplay
10+
} from './components'
11+
import { ImportCard } from '../shared'
12+
import {
13+
useFileSelection,
14+
useSnippetSelection,
15+
useImportWorkflow
16+
} from './hooks'
17+
18+
type DuplicateAction = 'ignore' | 'replace' | 'skip'
19+
type Step = 'upload' | 'select'
20+
21+
export const FileUploadForm: React.FC = () => {
22+
const [duplicateAction, setDuplicateAction] = useState<DuplicateAction>('ignore')
23+
const [currentStep, setCurrentStep] = useState<Step>('upload')
24+
const selectSectionRef = useRef<HTMLDivElement>(null)
25+
26+
const fileSelection = useFileSelection()
27+
const importWorkflow = useImportWorkflow()
28+
const snippetSelection = useSnippetSelection(importWorkflow.availableSnippets)
29+
30+
useEffect(() => {
31+
if (currentStep === 'select' && selectSectionRef.current) {
32+
selectSectionRef.current.scrollIntoView({
33+
behavior: 'smooth',
34+
block: 'start'
35+
})
36+
}
37+
}, [currentStep])
38+
39+
const handleFileSelect = (files: FileList | null) => {
40+
fileSelection.handleFileSelect(files)
41+
importWorkflow.clearUploadResult()
42+
}
43+
44+
const handleParseFiles = async () => {
45+
if (!fileSelection.selectedFiles) return
46+
47+
const success = await importWorkflow.parseFiles(fileSelection.selectedFiles)
48+
if (success) {
49+
snippetSelection.clearSelection()
50+
setCurrentStep('select')
51+
}
52+
}
53+
54+
const handleImportSelected = async () => {
55+
const snippetsToImport = snippetSelection.getSelectedSnippets()
56+
await importWorkflow.importSnippets(snippetsToImport, duplicateAction)
57+
}
58+
59+
const handleBackToUpload = () => {
60+
setCurrentStep('upload')
61+
fileSelection.clearFiles()
62+
snippetSelection.clearSelection()
63+
importWorkflow.resetWorkflow()
64+
}
65+
66+
const isUploadDisabled = !fileSelection.selectedFiles ||
67+
fileSelection.selectedFiles.length === 0 ||
68+
importWorkflow.isUploading
69+
70+
const isImportDisabled = snippetSelection.selectedSnippets.size === 0 ||
71+
importWorkflow.isImporting
72+
73+
return (
74+
<div className="wrap">
75+
<div className="import-form-container" style={{ maxWidth: '800px' }}>
76+
<p>{__('Upload one or more Code Snippets export files and the snippets will be imported.', 'code-snippets')}</p>
77+
78+
<p>
79+
{__('Afterward, you will need to visit the ', 'code-snippets')}
80+
<a href="admin.php?page=snippets">
81+
{__('All Snippets', 'code-snippets')}
82+
</a>
83+
{__(' page to activate the imported snippets.', 'code-snippets')}
84+
</p>
85+
86+
{currentStep === 'upload' && (
87+
<>
88+
89+
{(!importWorkflow.uploadResult || !importWorkflow.uploadResult.success) && (
90+
<>
91+
<DuplicateActionSelector
92+
value={duplicateAction}
93+
onChange={setDuplicateAction}
94+
/>
95+
96+
<ImportCard>
97+
<h2 style={{ margin: '0 0 1em 0' }}>{__('Choose Files', 'code-snippets')}</h2>
98+
<p className="description" style={{ marginBottom: '1em' }}>
99+
{__('Choose one or more Code Snippets (.xml or .json) files to parse and preview.', 'code-snippets')}
100+
</p>
101+
102+
<DragDropUploadArea
103+
fileInputRef={fileSelection.fileInputRef}
104+
onFileSelect={handleFileSelect}
105+
disabled={importWorkflow.isUploading}
106+
/>
107+
108+
{fileSelection.selectedFiles && fileSelection.selectedFiles.length > 0 && (
109+
<SelectedFilesList
110+
files={fileSelection.selectedFiles}
111+
onRemoveFile={fileSelection.removeFile}
112+
/>
113+
)}
114+
115+
<div style={{ textAlign: 'center' }}>
116+
<Button
117+
primary
118+
onClick={handleParseFiles}
119+
disabled={isUploadDisabled}
120+
style={{ minWidth: '200px' }}
121+
>
122+
{importWorkflow.isUploading
123+
? __('Uploading files...', 'code-snippets')
124+
: __('Upload files', 'code-snippets')
125+
}
126+
</Button>
127+
</div>
128+
</ImportCard>
129+
</>
130+
)}
131+
</>
132+
)}
133+
134+
{currentStep === 'select' && importWorkflow.availableSnippets.length > 0 && !importWorkflow.uploadResult?.success && (
135+
<ImportCard ref={selectSectionRef}>
136+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px' }}>
137+
<Button onClick={handleBackToUpload} className="button-link">
138+
{__('← Upload Different Files', 'code-snippets')}
139+
</Button>
140+
</div>
141+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
142+
<div>
143+
<h3 style={{ margin: '0' }}>{__('Available Snippets', 'code-snippets')} ({importWorkflow.availableSnippets.length})</h3>
144+
<p style={{ margin: '0.5em 0 1em 0', color: '#666' }}>
145+
{__('Select the snippets you want to import:', 'code-snippets')}
146+
</p>
147+
</div>
148+
<div>
149+
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
150+
{snippetSelection.isAllSelected
151+
? __('Deselect All', 'code-snippets')
152+
: __('Select All', 'code-snippets')
153+
}
154+
</Button>
155+
<Button
156+
primary
157+
onClick={handleImportSelected}
158+
disabled={isImportDisabled}
159+
>
160+
{importWorkflow.isImporting
161+
? __('Importing...', 'code-snippets')
162+
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
163+
</Button>
164+
</div>
165+
</div>
166+
167+
<SnippetSelectionTable
168+
snippets={importWorkflow.availableSnippets}
169+
selectedSnippets={snippetSelection.selectedSnippets}
170+
isAllSelected={snippetSelection.isAllSelected}
171+
onSnippetToggle={snippetSelection.handleSnippetToggle}
172+
onSelectAll={snippetSelection.handleSelectAll}
173+
/>
174+
175+
<div style={{ textAlign: 'end', marginTop: '1em' }}>
176+
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
177+
{snippetSelection.isAllSelected
178+
? __('Deselect All', 'code-snippets')
179+
: __('Select All', 'code-snippets')
180+
}
181+
</Button>
182+
<Button
183+
primary
184+
onClick={handleImportSelected}
185+
disabled={isImportDisabled}
186+
>
187+
{importWorkflow.isImporting
188+
? __('Importing...', 'code-snippets')
189+
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
190+
</Button>
191+
</div>
192+
</ImportCard>
193+
)}
194+
195+
{importWorkflow.uploadResult && (
196+
<ImportResultDisplay result={importWorkflow.uploadResult} />
197+
)}
198+
</div>
199+
</div>
200+
)
201+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react'
2+
import { __ } from '@wordpress/i18n'
3+
import { useDragAndDrop } from '../hooks/useDragAndDrop'
4+
5+
interface DragDropUploadAreaProps {
6+
fileInputRef: React.RefObject<HTMLInputElement>
7+
onFileSelect: (files: FileList | null) => void
8+
disabled?: boolean
9+
}
10+
11+
export const DragDropUploadArea: React.FC<DragDropUploadAreaProps> = ({
12+
fileInputRef,
13+
onFileSelect,
14+
disabled = false
15+
}) => {
16+
const { dragOver, handleDragOver, handleDragLeave, handleDrop } = useDragAndDrop({
17+
onFilesDrop: onFileSelect
18+
})
19+
20+
const handleClick = () => {
21+
if (!disabled) {
22+
fileInputRef.current?.click()
23+
}
24+
}
25+
26+
return (
27+
<>
28+
<div
29+
className={`upload-drop-zone ${dragOver ? 'drag-over' : ''}`}
30+
onDragOver={handleDragOver}
31+
onDragLeave={handleDragLeave}
32+
onDrop={handleDrop}
33+
onClick={handleClick}
34+
style={{
35+
border: `2px dashed ${dragOver ? '#0073aa' : '#ccd0d4'}`,
36+
borderRadius: '4px',
37+
padding: '40px 20px',
38+
textAlign: 'center',
39+
cursor: disabled ? 'not-allowed' : 'pointer',
40+
backgroundColor: dragOver ? '#f0f6fc' : disabled ? '#f6f7f7' : '#fafafa',
41+
marginBottom: '20px',
42+
transition: 'all 0.3s ease',
43+
opacity: disabled ? 0.6 : 1
44+
}}
45+
>
46+
<div style={{ fontSize: '48px', marginBottom: '10px', color: '#666' }}>📁</div>
47+
<p style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '500' }}>
48+
{__('Drag and drop files here, or click to browse', 'code-snippets')}
49+
</p>
50+
<p style={{ margin: '0', color: '#666', fontSize: '14px' }}>
51+
{__('Supports JSON and XML files', 'code-snippets')}
52+
</p>
53+
</div>
54+
55+
<input
56+
ref={fileInputRef}
57+
type="file"
58+
accept="application/json,.json,text/xml"
59+
multiple
60+
onChange={(e) => onFileSelect(e.target.files)}
61+
style={{ display: 'none' }}
62+
disabled={disabled}
63+
/>
64+
</>
65+
)
66+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react'
2+
import { __ } from '@wordpress/i18n'
3+
import { ImportCard } from '../../shared'
4+
5+
type DuplicateAction = 'ignore' | 'replace' | 'skip'
6+
7+
interface DuplicateActionSelectorProps {
8+
value: DuplicateAction
9+
onChange: (action: DuplicateAction) => void
10+
}
11+
12+
export const DuplicateActionSelector: React.FC<DuplicateActionSelectorProps> = ({
13+
value,
14+
onChange
15+
}) => {
16+
return (
17+
<ImportCard>
18+
<h2 style={{ margin: '0 0 1em 0' }}>{__('Duplicate Snippets', 'code-snippets')}</h2>
19+
<p className="description" style={{ marginBottom: '1em' }}>
20+
{__('What should happen if an existing snippet is found with an identical name to an imported snippet?', 'code-snippets')}
21+
</p>
22+
23+
<fieldset>
24+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
25+
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
26+
<input
27+
type="radio"
28+
name="duplicate_action"
29+
value="ignore"
30+
checked={value === 'ignore'}
31+
onChange={(e) => onChange(e.target.value as DuplicateAction)}
32+
style={{ marginTop: '2px' }}
33+
/>
34+
<span>
35+
{__('Ignore any duplicate snippets: import all snippets from the file regardless and leave all existing snippets unchanged.', 'code-snippets')}
36+
</span>
37+
</label>
38+
39+
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
40+
<input
41+
type="radio"
42+
name="duplicate_action"
43+
value="replace"
44+
checked={value === 'replace'}
45+
onChange={(e) => onChange(e.target.value as DuplicateAction)}
46+
style={{ marginTop: '2px' }}
47+
/>
48+
<span>
49+
{__('Replace any existing snippets with a newly imported snippet of the same name.', 'code-snippets')}
50+
</span>
51+
</label>
52+
53+
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
54+
<input
55+
type="radio"
56+
name="duplicate_action"
57+
value="skip"
58+
checked={value === 'skip'}
59+
onChange={(e) => onChange(e.target.value as DuplicateAction)}
60+
style={{ marginTop: '2px' }}
61+
/>
62+
<span>
63+
{__('Do not import any duplicate snippets; leave all existing snippets unchanged.', 'code-snippets')}
64+
</span>
65+
</label>
66+
</div>
67+
</fieldset>
68+
</ImportCard>
69+
)
70+
}

0 commit comments

Comments
 (0)