Initial commit: WordPress wp-content (themes, plugins, languages)
- Theme: momentry (custom theme with REST API routes) - Plugins: code-snippets (contains all API proxies) - Languages: zh_TW translations - Excludes: cache, backups, uploads, logs
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { ImportCard } from '../../shared'
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean
|
||||
message: string
|
||||
imported?: number
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
interface ImportResultDisplayProps {
|
||||
result: ImportResult
|
||||
}
|
||||
|
||||
export const ImportResultDisplay: React.FC<ImportResultDisplayProps> = ({ result }) => {
|
||||
return (
|
||||
<ImportCard>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
|
||||
<div style={{
|
||||
backgroundColor: result.success ? '#00a32a' : '#d63638',
|
||||
borderRadius: '50%',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
<span style={{ color: 'white', fontSize: '14px', fontWeight: 'bold' }}>
|
||||
{result.success ? '✓' : '✕'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '600' }}>
|
||||
{result.success
|
||||
? __('Import Successful!', 'code-snippets')
|
||||
: __('Import Failed', 'code-snippets')
|
||||
}
|
||||
</h3>
|
||||
<p style={{ margin: '0 0 8px 0', color: '#666' }}>
|
||||
{result.message}
|
||||
</p>
|
||||
|
||||
{result.success && (
|
||||
<p style={{ margin: '0', color: '#666' }}>
|
||||
{__('Go to ', 'code-snippets')}
|
||||
<a href="admin.php?page=snippets" style={{ color: '#2271b1', textDecoration: 'none' }}>
|
||||
{__('All Snippets', 'code-snippets')}
|
||||
</a>
|
||||
{__(' to activate your imported snippets.', 'code-snippets')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{result.warnings && result.warnings.length > 0 && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#d63638' }}>
|
||||
{__('Warnings:', 'code-snippets')}
|
||||
</h4>
|
||||
<ul style={{ margin: '0', paddingLeft: '20px' }}>
|
||||
{result.warnings.map((warning, index) => (
|
||||
<li key={index} style={{ color: '#666', fontSize: '14px' }}>
|
||||
{warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ImportCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { formatFileSize } from '../utils/fileUtils'
|
||||
|
||||
interface SelectedFilesListProps {
|
||||
files: FileList
|
||||
onRemoveFile: (index: number) => void
|
||||
}
|
||||
|
||||
export const SelectedFilesList: React.FC<SelectedFilesListProps> = ({
|
||||
files,
|
||||
onRemoveFile
|
||||
}) => {
|
||||
return (
|
||||
<div className="selected-files" style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: '600' }}>
|
||||
{__('Selected Files:', 'code-snippets')} ({files.length})
|
||||
</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{Array.from(files).map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '16px' }}>📄</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: '500' }}>{file.name}</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{formatFileSize(file.size)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemoveFile(index)
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#d63638',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
padding: '4px'
|
||||
}}
|
||||
title={__('Remove file', 'code-snippets')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
|
||||
|
||||
interface SnippetSelectionTableProps {
|
||||
snippets: ImportableSnippet[]
|
||||
selectedSnippets: Set<number | string>
|
||||
isAllSelected: boolean
|
||||
onSnippetToggle: (snippetId: number | string) => void
|
||||
onSelectAll: () => void
|
||||
}
|
||||
|
||||
export const SnippetSelectionTable: React.FC<SnippetSelectionTableProps> = ({
|
||||
snippets,
|
||||
selectedSnippets,
|
||||
isAllSelected,
|
||||
onSnippetToggle,
|
||||
onSelectAll
|
||||
}) => {
|
||||
const getTypeColor = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'css': return '#9B59B6'
|
||||
case 'js': return '#FFEB3B'
|
||||
case 'html': return '#EF6A36'
|
||||
default: return '#1D97C6'
|
||||
}
|
||||
}
|
||||
|
||||
const truncateDescription = (description: string | undefined): string => {
|
||||
const desc = description || __('No description', 'code-snippets')
|
||||
return desc.length > 50 ? desc.substring(0, 50) + '...' : desc
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="wp-list-table widefat fixed striped" style={{ borderRadius: '5px', tableLayout: 'fixed' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="check-column" style={{ padding: '8px 0', width: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAllSelected}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th scope="col" style={{ width: '200px' }}>{__('Name', 'code-snippets')}</th>
|
||||
<th scope="col" style={{ width: '90px', textAlign: 'center' }}>{__('Type', 'code-snippets')}</th>
|
||||
<th scope="col" style={{ width: 'auto' }}>{__('Description', 'code-snippets')}</th>
|
||||
<th scope="col" style={{ width: '120px' }}>{__('Tags', 'code-snippets')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snippets.map(snippet => (
|
||||
<tr key={snippet.table_data.id}>
|
||||
<th scope="row" className="check-column">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSnippets.has(snippet.table_data.id)}
|
||||
onChange={() => onSnippetToggle(snippet.table_data.id)}
|
||||
/>
|
||||
</th>
|
||||
<td>
|
||||
<strong>{snippet.table_data.title}</strong>
|
||||
{snippet.source_file && (
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
|
||||
from {snippet.source_file}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ width: '90px', textAlign: 'center' }}>
|
||||
<span style={{
|
||||
backgroundColor: getTypeColor(snippet.table_data.type),
|
||||
color: 'white',
|
||||
padding: '3px 6px',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: '3px'
|
||||
}}>
|
||||
{snippet.table_data.type}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{truncateDescription(snippet.table_data.description)}
|
||||
</td>
|
||||
<td>{snippet.table_data.tags || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { DuplicateActionSelector } from './DuplicateActionSelector'
|
||||
export { DragDropUploadArea } from './DragDropUploadArea'
|
||||
export { SelectedFilesList } from './SelectedFilesList'
|
||||
export { SnippetSelectionTable } from './SnippetSelectionTable'
|
||||
export { ImportResultDisplay } from './ImportResultDisplay'
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useDragAndDrop } from './useDragAndDrop'
|
||||
export { useFileSelection } from './useFileSelection'
|
||||
export { useSnippetSelection } from './useSnippetSelection'
|
||||
export { useImportWorkflow } from './useImportWorkflow'
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface UseDragAndDropProps {
|
||||
onFilesDrop: (files: FileList) => void
|
||||
}
|
||||
|
||||
export const useDragAndDrop = ({ onFilesDrop }: UseDragAndDropProps) => {
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length > 0) {
|
||||
onFilesDrop(files)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dragOver,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { removeFileFromList } from '../utils/fileUtils'
|
||||
|
||||
export const useFileSelection = () => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
setSelectedFiles(files)
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
if (!selectedFiles) return
|
||||
|
||||
const newFiles = removeFileFromList(selectedFiles, index)
|
||||
setSelectedFiles(newFiles)
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.files = newFiles
|
||||
}
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
setSelectedFiles(null)
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
return {
|
||||
selectedFiles,
|
||||
fileInputRef,
|
||||
handleFileSelect,
|
||||
removeFile,
|
||||
clearFiles,
|
||||
triggerFileInput
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useState } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useFileUploadAPI, type ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
|
||||
import { isNetworkAdmin } from '../../../../utils/screen'
|
||||
|
||||
type DuplicateAction = 'ignore' | 'replace' | 'skip'
|
||||
|
||||
interface UploadResult {
|
||||
success: boolean
|
||||
message: string
|
||||
imported?: number
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
export const useImportWorkflow = () => {
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [availableSnippets, setAvailableSnippets] = useState<ImportableSnippet[]>([])
|
||||
const [uploadResult, setUploadResult] = useState<UploadResult | null>(null)
|
||||
|
||||
const fileUploadAPI = useFileUploadAPI()
|
||||
|
||||
const parseFiles = async (files: FileList): Promise<boolean> => {
|
||||
if (!files || files.length === 0) {
|
||||
alert(__('Please select files to upload.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
|
||||
setIsUploading(true)
|
||||
setUploadResult(null)
|
||||
|
||||
try {
|
||||
const response = await fileUploadAPI.parseFiles({ files })
|
||||
|
||||
setAvailableSnippets(response.data.snippets)
|
||||
|
||||
if (response.data.warnings && response.data.warnings.length > 0) {
|
||||
setUploadResult({
|
||||
success: true,
|
||||
message: response.data.message,
|
||||
warnings: response.data.warnings
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error('Parse error:', error)
|
||||
setUploadResult({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : __('An unknown error occurred.', 'code-snippets')
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const importSnippets = async (
|
||||
snippetsToImport: ImportableSnippet[],
|
||||
duplicateAction: DuplicateAction
|
||||
): Promise<boolean> => {
|
||||
if (snippetsToImport.length === 0) {
|
||||
alert(__('Please select snippets to import.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
setUploadResult(null)
|
||||
|
||||
try {
|
||||
const response = await fileUploadAPI.importSnippets({
|
||||
snippets: snippetsToImport,
|
||||
duplicate_action: duplicateAction,
|
||||
network: isNetworkAdmin()
|
||||
})
|
||||
|
||||
setUploadResult({
|
||||
success: true,
|
||||
message: response.data.message,
|
||||
imported: response.data.imported
|
||||
})
|
||||
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
setUploadResult({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : __('An unknown error occurred.', 'code-snippets')
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetWorkflow = () => {
|
||||
setAvailableSnippets([])
|
||||
setUploadResult(null)
|
||||
}
|
||||
|
||||
const clearUploadResult = () => {
|
||||
setUploadResult(null)
|
||||
}
|
||||
|
||||
return {
|
||||
isUploading,
|
||||
isImporting,
|
||||
availableSnippets,
|
||||
uploadResult,
|
||||
parseFiles,
|
||||
importSnippets,
|
||||
resetWorkflow,
|
||||
clearUploadResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react'
|
||||
import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
|
||||
|
||||
export const useSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
|
||||
const [selectedSnippets, setSelectedSnippets] = useState<Set<number | string>>(new Set())
|
||||
|
||||
const handleSnippetToggle = (snippetId: number | string) => {
|
||||
const newSelected = new Set(selectedSnippets)
|
||||
if (newSelected.has(snippetId)) {
|
||||
newSelected.delete(snippetId)
|
||||
} else {
|
||||
newSelected.add(snippetId)
|
||||
}
|
||||
setSelectedSnippets(newSelected)
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedSnippets.size === availableSnippets.length) {
|
||||
setSelectedSnippets(new Set())
|
||||
} else {
|
||||
setSelectedSnippets(new Set(availableSnippets.map(snippet => snippet.table_data.id)))
|
||||
}
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedSnippets(new Set())
|
||||
}
|
||||
|
||||
const getSelectedSnippets = () => {
|
||||
return availableSnippets.filter(snippet =>
|
||||
selectedSnippets.has(snippet.table_data.id)
|
||||
)
|
||||
}
|
||||
|
||||
const isAllSelected = selectedSnippets.size === availableSnippets.length && availableSnippets.length > 0
|
||||
|
||||
return {
|
||||
selectedSnippets,
|
||||
handleSnippetToggle,
|
||||
handleSelectAll,
|
||||
clearSelection,
|
||||
getSelectedSnippets,
|
||||
isAllSelected
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export const removeFileFromList = (fileList: FileList, indexToRemove: number): FileList => {
|
||||
const dt = new DataTransfer()
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
if (i !== indexToRemove) {
|
||||
dt.items.add(fileList[i])
|
||||
}
|
||||
}
|
||||
return dt.files
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { useState } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import {
|
||||
ImporterSelector,
|
||||
ImportOptions,
|
||||
SimpleSnippetTable,
|
||||
StatusDisplay
|
||||
} from './components'
|
||||
import { ImportCard } from '../shared'
|
||||
import {
|
||||
useImporterSelection,
|
||||
useSnippetImport,
|
||||
useImportSnippetSelection
|
||||
} from './hooks'
|
||||
|
||||
export const ImportForm: React.FC = () => {
|
||||
const [autoAddTags, setAutoAddTags] = useState<boolean>(false)
|
||||
|
||||
const importerSelection = useImporterSelection()
|
||||
const snippetImport = useSnippetImport()
|
||||
const snippetSelection = useImportSnippetSelection(snippetImport.snippets)
|
||||
|
||||
const handleImporterChange = async (newImporter: string) => {
|
||||
importerSelection.handleImporterChange(newImporter)
|
||||
snippetSelection.clearSelection()
|
||||
snippetImport.resetAll()
|
||||
|
||||
if (newImporter) {
|
||||
await snippetImport.loadSnippets(newImporter)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
const selectedIds = Array.from(snippetSelection.selectedSnippets)
|
||||
const success = await snippetImport.importSnippets(
|
||||
importerSelection.selectedImporter,
|
||||
selectedIds,
|
||||
autoAddTags,
|
||||
importerSelection.tagValue
|
||||
)
|
||||
|
||||
if (success) {
|
||||
snippetSelection.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
if (importerSelection.isLoading) {
|
||||
return (
|
||||
<div className="wrap">
|
||||
<p>{__('Loading importers...', 'code-snippets')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (importerSelection.error) {
|
||||
return (
|
||||
<div className="wrap">
|
||||
<div className="notice notice-error">
|
||||
<p>{__('Error loading importers:', 'code-snippets')} {importerSelection.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wrap">
|
||||
<div className="import-form-container" style={{ maxWidth: '800px' }}>
|
||||
<p>{__('If you are using another Snippets plugin, you can import all existing snippets to your Code Snippets library.', 'code-snippets')}</p>
|
||||
|
||||
<ImporterSelector
|
||||
importers={importerSelection.importers}
|
||||
selectedImporter={importerSelection.selectedImporter}
|
||||
onImporterChange={handleImporterChange}
|
||||
isLoading={snippetImport.isLoadingSnippets}
|
||||
/>
|
||||
|
||||
{snippetImport.snippetsError && (
|
||||
<StatusDisplay
|
||||
type="error"
|
||||
title={__('Error loading snippets', 'code-snippets')}
|
||||
message={snippetImport.snippetsError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{snippetImport.importError && (
|
||||
<StatusDisplay
|
||||
type="error"
|
||||
title={__('Error importing snippets', 'code-snippets')}
|
||||
message={snippetImport.importError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{snippetImport.importSuccess.length > 0 && (
|
||||
<StatusDisplay
|
||||
type="success"
|
||||
title={`${snippetImport.importSuccess.length} ${__('Snippets imported!', 'code-snippets')}`}
|
||||
message={__('We successfully imported all snippets to your library. Go to ', 'code-snippets')}
|
||||
showSnippetsLink
|
||||
/>
|
||||
)}
|
||||
|
||||
{importerSelection.selectedImporter &&
|
||||
!snippetImport.isLoadingSnippets &&
|
||||
!snippetImport.snippetsError &&
|
||||
snippetImport.snippets.length === 0 &&
|
||||
snippetImport.importSuccess.length === 0 && (
|
||||
<ImportCard>
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#666' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📭</div>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '18px', color: '#333' }}>
|
||||
{__('No snippets found', 'code-snippets')}
|
||||
</h3>
|
||||
<p style={{ margin: '0', fontSize: '14px' }}>
|
||||
{__('No snippets were found for the selected plugin. Make sure the plugin is installed and has snippets configured.', 'code-snippets')}
|
||||
</p>
|
||||
</div>
|
||||
</ImportCard>
|
||||
)}
|
||||
|
||||
{snippetImport.snippets.length > 0 && (
|
||||
<>
|
||||
<ImportOptions
|
||||
autoAddTags={autoAddTags}
|
||||
tagValue={importerSelection.tagValue}
|
||||
onAutoAddTagsChange={setAutoAddTags}
|
||||
onTagValueChange={importerSelection.setTagValue}
|
||||
/>
|
||||
|
||||
<SimpleSnippetTable
|
||||
snippets={snippetImport.snippets}
|
||||
selectedSnippets={snippetSelection.selectedSnippets}
|
||||
onSnippetToggle={snippetSelection.handleSnippetToggle}
|
||||
onSelectAll={snippetSelection.handleSelectAll}
|
||||
onImport={handleImport}
|
||||
isImporting={snippetImport.isImporting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { ImportCard } from '../../shared'
|
||||
|
||||
interface ImportOptionsProps {
|
||||
autoAddTags: boolean
|
||||
tagValue: string
|
||||
onAutoAddTagsChange: (enabled: boolean) => void
|
||||
onTagValueChange: (value: string) => void
|
||||
}
|
||||
|
||||
export const ImportOptions: React.FC<ImportOptionsProps> = ({
|
||||
autoAddTags,
|
||||
tagValue,
|
||||
onAutoAddTagsChange,
|
||||
onTagValueChange
|
||||
}) => {
|
||||
return (
|
||||
<ImportCard>
|
||||
<h2 style={{ margin: '0 0 1em 0' }}>{__('Import options', 'code-snippets')}</h2>
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoAddTags}
|
||||
onChange={(e) => onAutoAddTagsChange(e.target.checked)}
|
||||
style={{ marginTop: '2px' }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div>
|
||||
<strong>{__('Automatically add Tag', 'code-snippets')}</strong>
|
||||
<br />
|
||||
<span style={{ color: '#666', fontSize: '0.9em' }}>
|
||||
{__('For your convenience, we can add a tag on every imported snippet.', 'code-snippets')}
|
||||
</span>
|
||||
</div>
|
||||
{autoAddTags && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={tagValue}
|
||||
onChange={(e) => onTagValueChange(e.target.value)}
|
||||
placeholder={__('Add tag...', 'code-snippets')}
|
||||
className="regular-text"
|
||||
style={{ width: '100%', maxWidth: '300px' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</ImportCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import type { Importer } from '../../../../hooks/useImportersAPI'
|
||||
import { ImportCard } from '../../shared'
|
||||
|
||||
interface ImporterSelectorProps {
|
||||
importers: Importer[]
|
||||
selectedImporter: string
|
||||
onImporterChange: (importerName: string) => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const ImporterSelector: React.FC<ImporterSelectorProps> = ({
|
||||
importers,
|
||||
selectedImporter,
|
||||
onImporterChange,
|
||||
isLoading
|
||||
}) => {
|
||||
return (
|
||||
<ImportCard variant="controls">
|
||||
<label htmlFor="importer-select">
|
||||
<h2 style={{ margin: '0 0 1em 0' }}>{__('Select Plugin', 'code-snippets')}</h2>
|
||||
</label>
|
||||
<select
|
||||
id="importer-select"
|
||||
value={selectedImporter}
|
||||
onChange={(event) => onImporterChange(event.target.value)}
|
||||
className="regular-text"
|
||||
style={{ display: 'block', marginTop: '5px', width: '100%', maxWidth: '300px' }}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<option value="">{__('-- Select an importer --', 'code-snippets')}</option>
|
||||
{importers.map(importer => (
|
||||
<option
|
||||
key={importer.name}
|
||||
value={importer.name}
|
||||
disabled={!importer.is_active}
|
||||
>
|
||||
{importer.title} {!importer.is_active ? __('(Inactive)', 'code-snippets') : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isLoading && (
|
||||
<p style={{ margin: '10px 0 0 0', color: '#666', fontSize: '14px' }}>
|
||||
{__('Loading snippets...', 'code-snippets')}
|
||||
</p>
|
||||
)}
|
||||
</ImportCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Button } from '../../../common/Button'
|
||||
import type { ImportableSnippet } from '../../../../hooks/useImportersAPI'
|
||||
import { ImportCard } from '../../shared'
|
||||
|
||||
interface SimpleSnippetTableProps {
|
||||
snippets: ImportableSnippet[]
|
||||
selectedSnippets: Set<number>
|
||||
onSnippetToggle: (snippetId: number) => void
|
||||
onSelectAll: () => void
|
||||
onImport: () => void
|
||||
isImporting: boolean
|
||||
}
|
||||
|
||||
export const SimpleSnippetTable: React.FC<SimpleSnippetTableProps> = ({
|
||||
snippets,
|
||||
selectedSnippets,
|
||||
onSnippetToggle,
|
||||
onSelectAll,
|
||||
onImport,
|
||||
isImporting
|
||||
}) => {
|
||||
const isAllSelected = selectedSnippets.size === snippets.length && snippets.length > 0
|
||||
|
||||
return (
|
||||
<ImportCard className="snippets-table-container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: '0' }}>{__('Available Snippets', 'code-snippets')} ({snippets.length})</h2>
|
||||
<p style={{ margin: '0.5em 0 1em 0' }}>{__('We found the following snippets.', 'code-snippets')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={onSelectAll} style={{ marginRight: '10px' }}>
|
||||
{isAllSelected
|
||||
? __('Deselect All', 'code-snippets')
|
||||
: __('Select All', 'code-snippets')
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
primary
|
||||
onClick={onImport}
|
||||
disabled={selectedSnippets.size === 0 || isImporting}
|
||||
>
|
||||
{isImporting
|
||||
? __('Importing...', 'code-snippets')
|
||||
: __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="wp-list-table widefat fixed striped" style={{ borderRadius: '5px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="check-column" style={{ padding: '8px 0' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAllSelected}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">{__('Snippet Name', 'code-snippets')}</th>
|
||||
<th scope="col" style={{ textAlign: 'end', width: '50px' }}>{__('ID', 'code-snippets')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snippets.map(snippet => (
|
||||
<tr key={snippet.table_data.id}>
|
||||
<th scope="row" className="check-column">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSnippets.has(snippet.table_data.id)}
|
||||
onChange={() => onSnippetToggle(snippet.table_data.id)}
|
||||
/>
|
||||
</th>
|
||||
<td>{snippet.table_data.title}</td>
|
||||
<td style={{ textAlign: 'end', width: '50px' }}>{snippet.table_data.id}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style={{ textAlign: 'end', marginTop: '1em' }}>
|
||||
<Button onClick={onSelectAll} style={{ marginRight: '10px' }}>
|
||||
{isAllSelected
|
||||
? __('Deselect All', 'code-snippets')
|
||||
: __('Select All', 'code-snippets')
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
primary
|
||||
onClick={onImport}
|
||||
disabled={selectedSnippets.size === 0 || isImporting}
|
||||
>
|
||||
{isImporting
|
||||
? __('Importing...', 'code-snippets')
|
||||
: __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
|
||||
</Button>
|
||||
</div>
|
||||
</ImportCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { ImportCard } from '../../shared'
|
||||
|
||||
interface StatusDisplayProps {
|
||||
type: 'error' | 'success'
|
||||
title: string
|
||||
message: string
|
||||
showSnippetsLink?: boolean
|
||||
}
|
||||
|
||||
export const StatusDisplay: React.FC<StatusDisplayProps> = ({
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
showSnippetsLink = false
|
||||
}) => {
|
||||
const isError = type === 'error'
|
||||
|
||||
return (
|
||||
<ImportCard variant="controls" style={{ display: 'flex', alignItems: 'flex-start', gap: '12px', marginBottom: '20px' }}>
|
||||
<div style={{
|
||||
backgroundColor: isError ? '#d63638' : '#00a32a',
|
||||
borderRadius: '50%',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
<span style={{ color: 'white', fontSize: '14px', fontWeight: 'bold' }}>
|
||||
{isError ? '✕' : '✓'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '600' }}>
|
||||
{title}
|
||||
</h3>
|
||||
<p style={{ margin: '0', color: '#666' }}>
|
||||
{message}
|
||||
{showSnippetsLink && (
|
||||
<>
|
||||
{' '}
|
||||
<a href="admin.php?page=snippets" style={{ color: '#2271b1', textDecoration: 'none' }}>
|
||||
{__('Code Snippets Library', 'code-snippets')}
|
||||
</a>.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</ImportCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { ImporterSelector } from './ImporterSelector'
|
||||
export { ImportOptions } from './ImportOptions'
|
||||
export { SimpleSnippetTable } from './SimpleSnippetTable'
|
||||
export { StatusDisplay } from './StatusDisplay'
|
||||
@@ -0,0 +1,3 @@
|
||||
export { useImporterSelection } from './useImporterSelection'
|
||||
export { useSnippetImport } from './useSnippetImport'
|
||||
export { useImportSnippetSelection } from './useImportSnippetSelection'
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react'
|
||||
import type { ImportableSnippet } from '../../../../hooks/useImportersAPI'
|
||||
|
||||
export const useImportSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
|
||||
const [selectedSnippets, setSelectedSnippets] = useState<Set<number>>(new Set())
|
||||
|
||||
const handleSnippetToggle = (snippetId: number) => {
|
||||
const newSelected = new Set(selectedSnippets)
|
||||
if (newSelected.has(snippetId)) {
|
||||
newSelected.delete(snippetId)
|
||||
} else {
|
||||
newSelected.add(snippetId)
|
||||
}
|
||||
setSelectedSnippets(newSelected)
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedSnippets.size === availableSnippets.length) {
|
||||
setSelectedSnippets(new Set())
|
||||
} else {
|
||||
setSelectedSnippets(new Set(availableSnippets.map(snippet => snippet.table_data.id)))
|
||||
}
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedSnippets(new Set())
|
||||
}
|
||||
|
||||
const getSelectedSnippets = () => {
|
||||
return availableSnippets.filter(snippet =>
|
||||
selectedSnippets.has(snippet.table_data.id)
|
||||
)
|
||||
}
|
||||
|
||||
const isAllSelected = selectedSnippets.size === availableSnippets.length && availableSnippets.length > 0
|
||||
|
||||
return {
|
||||
selectedSnippets,
|
||||
handleSnippetToggle,
|
||||
handleSelectAll,
|
||||
clearSelection,
|
||||
getSelectedSnippets,
|
||||
isAllSelected
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useImportersAPI, type Importer } from '../../../../hooks/useImportersAPI'
|
||||
|
||||
export const useImporterSelection = () => {
|
||||
const [importers, setImporters] = useState<Importer[]>([])
|
||||
const [selectedImporter, setSelectedImporter] = useState<string>('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [tagValue, setTagValue] = useState<string>('')
|
||||
|
||||
const importersAPI = useImportersAPI()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchImporters = async () => {
|
||||
try {
|
||||
const response = await importersAPI.fetchAll()
|
||||
setImporters(response.data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchImporters()
|
||||
}, [importersAPI])
|
||||
|
||||
const handleImporterChange = (newImporter: string) => {
|
||||
setSelectedImporter(newImporter)
|
||||
setTagValue(`imported-${newImporter}`)
|
||||
}
|
||||
|
||||
return {
|
||||
importers,
|
||||
selectedImporter,
|
||||
isLoading,
|
||||
error,
|
||||
tagValue,
|
||||
setTagValue,
|
||||
handleImporterChange
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { useImportersAPI, type ImportableSnippet } from '../../../../hooks/useImportersAPI'
|
||||
import { isNetworkAdmin } from '../../../../utils/screen'
|
||||
|
||||
export const useSnippetImport = () => {
|
||||
const [snippets, setSnippets] = useState<ImportableSnippet[]>([])
|
||||
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false)
|
||||
const [snippetsError, setSnippetsError] = useState<string | null>(null)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [importError, setImportError] = useState<string | null>(null)
|
||||
const [importSuccess, setImportSuccess] = useState<number[]>([])
|
||||
|
||||
const importersAPI = useImportersAPI()
|
||||
|
||||
const loadSnippets = async (importerName: string): Promise<boolean> => {
|
||||
if (!importerName) {
|
||||
alert(__('Please select an importer.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
|
||||
setIsLoadingSnippets(true)
|
||||
setSnippetsError(null)
|
||||
setSnippets([])
|
||||
clearResults()
|
||||
|
||||
try {
|
||||
const response = await importersAPI.fetchSnippets(importerName)
|
||||
setSnippets(response.data)
|
||||
return true
|
||||
} catch (err) {
|
||||
setSnippetsError(err instanceof Error ? err.message : 'Unknown error')
|
||||
return false
|
||||
} finally {
|
||||
setIsLoadingSnippets(false)
|
||||
}
|
||||
}
|
||||
|
||||
const importSnippets = async (
|
||||
importerName: string,
|
||||
selectedSnippetIds: number[],
|
||||
autoAddTags: boolean,
|
||||
tagValue: string
|
||||
): Promise<boolean> => {
|
||||
if (selectedSnippetIds.length === 0) {
|
||||
alert(__('Please select snippets to import.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!importerName) {
|
||||
alert(__('Please select an importer.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
setImportError(null)
|
||||
setImportSuccess([])
|
||||
|
||||
try {
|
||||
const response = await importersAPI.importSnippets(importerName, {
|
||||
ids: selectedSnippetIds,
|
||||
network: isNetworkAdmin(),
|
||||
auto_add_tags: autoAddTags,
|
||||
tag_value: autoAddTags ? tagValue : undefined
|
||||
})
|
||||
|
||||
setImportSuccess(response.data.imported)
|
||||
|
||||
if (response.data.imported.length > 0) {
|
||||
setSnippets([])
|
||||
return true
|
||||
} else {
|
||||
alert(__('No snippets were imported.', 'code-snippets'))
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
setImportError(err instanceof Error ? err.message : 'Unknown error')
|
||||
return false
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const clearResults = () => {
|
||||
setImportSuccess([])
|
||||
setImportError(null)
|
||||
}
|
||||
|
||||
const resetAll = () => {
|
||||
setSnippets([])
|
||||
clearResults()
|
||||
setSnippetsError(null)
|
||||
}
|
||||
|
||||
return {
|
||||
snippets,
|
||||
isLoadingSnippets,
|
||||
snippetsError,
|
||||
isImporting,
|
||||
importError,
|
||||
importSuccess,
|
||||
loadSnippets,
|
||||
importSnippets,
|
||||
clearResults,
|
||||
resetAll
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ImportForm'
|
||||
62
plugins/code-snippets/js/components/Import/ImportApp.tsx
Normal file
62
plugins/code-snippets/js/components/Import/ImportApp.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { FileUploadForm } from './FromFileUpload/FileUploadForm'
|
||||
import { ImportForm } from './FromOtherPlugins/ImportForm'
|
||||
import { ImportSection } from './shared'
|
||||
|
||||
type TabType = 'upload' | 'plugins'
|
||||
|
||||
export const ImportApp: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('upload')
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const tabParam = urlParams.get('tab') as TabType
|
||||
if (tabParam === 'plugins' || tabParam === 'upload') {
|
||||
setActiveTab(tabParam)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTabChange = (tab: TabType) => {
|
||||
setActiveTab(tab)
|
||||
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('tab', tab)
|
||||
window.history.replaceState({}, '', url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="narrow" style={{ maxWidth: '800px' }}>
|
||||
<h2 className="nav-tab-wrapper" style={{ marginBottom: '20px' }}>
|
||||
<a
|
||||
className={`nav-tab${activeTab === 'upload' ? ' nav-tab-active' : ''}`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleTabChange('upload')
|
||||
}}
|
||||
>
|
||||
{__('Import Snippets', 'code-snippets')}
|
||||
</a>
|
||||
<a
|
||||
className={`nav-tab${activeTab === 'plugins' ? ' nav-tab-active' : ''}`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleTabChange('plugins')
|
||||
}}
|
||||
>
|
||||
{__('Import from other plugins', 'code-snippets')}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<ImportSection active={activeTab === 'upload'}>
|
||||
<FileUploadForm />
|
||||
</ImportSection>
|
||||
|
||||
<ImportSection active={activeTab === 'plugins'}>
|
||||
<ImportForm />
|
||||
</ImportSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export interface ImportCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
variant?: 'default' | 'controls'
|
||||
}
|
||||
|
||||
export const ImportCard = React.forwardRef<HTMLDivElement, ImportCardProps>(({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
style,
|
||||
...props
|
||||
}, ref) => {
|
||||
const cardStyle: React.CSSProperties = {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '25px',
|
||||
borderRadius: '5px',
|
||||
border: '1px solid #e0e0e0',
|
||||
marginBottom: '10px',
|
||||
width: '100%',
|
||||
...style
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
{
|
||||
'import-controls': variant === 'controls'
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={cardStyle}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
ImportCard.displayName = 'ImportCard'
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export interface ImportSectionProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
|
||||
children: React.ReactNode
|
||||
active?: boolean
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const ImportSection: React.FC<ImportSectionProps> = ({
|
||||
children,
|
||||
active = false,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const sectionStyle: React.CSSProperties = {
|
||||
display: active ? 'block' : 'none',
|
||||
paddingTop: 0,
|
||||
...style
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={sectionStyle}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { ImportCard } from './ImportCard'
|
||||
export type { ImportCardProps } from './ImportCard'
|
||||
export { ImportSection } from './ImportSection'
|
||||
export type { ImportSectionProps } from './ImportSection'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './components'
|
||||
Reference in New Issue
Block a user