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,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'
|
||||
Reference in New Issue
Block a user