From c160afe0875a658b3c30db836acb1f4be7cee310 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 27 Nov 2025 21:48:59 +0200
Subject: [PATCH 01/31] feat: import snippets
---
config/webpack-js.ts | 1 +
.../Import/FromFileUpload/FileUploadForm.tsx | 201 +++++++++
.../components/DragDropUploadArea.tsx | 66 +++
.../components/DuplicateActionSelector.tsx | 70 +++
.../components/ImportResultDisplay.tsx | 74 ++++
.../components/SelectedFilesList.tsx | 65 +++
.../components/SnippetSelectionTable.tsx | 90 ++++
.../Import/FromFileUpload/components/index.ts | 5 +
.../Import/FromFileUpload/hooks/index.ts | 4 +
.../FromFileUpload/hooks/useDragAndDrop.ts | 36 ++
.../FromFileUpload/hooks/useFileSelection.ts | 42 ++
.../FromFileUpload/hooks/useImportWorkflow.ts | 117 +++++
.../hooks/useSnippetSelection.ts | 45 ++
.../Import/FromFileUpload/utils/fileUtils.ts | 17 +
.../Import/FromOtherPlugins/ImportForm.tsx | 142 ++++++
.../components/ImportOptions.tsx | 52 +++
.../components/ImporterSelector.tsx | 50 +++
.../components/SimpleSnippetTable.tsx | 102 +++++
.../components/StatusDisplay.tsx | 55 +++
.../FromOtherPlugins/components/index.ts | 4 +
.../Import/FromOtherPlugins/hooks/index.ts | 3 +
.../hooks/useImportSnippetSelection.ts | 45 ++
.../hooks/useImporterSelection.ts | 42 ++
.../hooks/useSnippetImport.ts | 107 +++++
.../Import/FromOtherPlugins/index.ts | 1 +
src/js/components/Import/ImportApp.tsx | 62 +++
.../Import/shared/components/ImportCard.tsx | 45 ++
.../shared/components/ImportSection.tsx | 33 ++
.../Import/shared/components/index.ts | 4 +
src/js/components/Import/shared/index.ts | 1 +
src/js/hooks/useAxios.ts | 39 ++
src/js/hooks/useFileUploadAPI.ts | 87 ++++
src/js/hooks/useImportersAPI.ts | 52 +++
src/js/import.tsx | 12 +
src/php/admin-menus/class-import-menu.php | 119 +----
src/php/class-plugin.php | 6 +-
.../importers/files/file-upload-importer.php | 406 ++++++++++++++++++
.../plugins/header-footer-code-manager.php | 149 +++++++
.../importers/plugins/importer-base.php | 126 ++++++
.../plugins/insert-headers-and-footers.php | 138 ++++++
.../plugins/insert-php-code-snippet.php | 125 ++++++
.../migration/importers/plugins/manager.php | 60 +++
src/php/views/import.php | 86 +---
43 files changed, 2798 insertions(+), 188 deletions(-)
create mode 100644 src/js/components/Import/FromFileUpload/FileUploadForm.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
create mode 100644 src/js/components/Import/FromFileUpload/components/index.ts
create mode 100644 src/js/components/Import/FromFileUpload/hooks/index.ts
create mode 100644 src/js/components/Import/FromFileUpload/hooks/useDragAndDrop.ts
create mode 100644 src/js/components/Import/FromFileUpload/hooks/useFileSelection.ts
create mode 100644 src/js/components/Import/FromFileUpload/hooks/useImportWorkflow.ts
create mode 100644 src/js/components/Import/FromFileUpload/hooks/useSnippetSelection.ts
create mode 100644 src/js/components/Import/FromFileUpload/utils/fileUtils.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/ImportForm.tsx
create mode 100644 src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
create mode 100644 src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
create mode 100644 src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
create mode 100644 src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
create mode 100644 src/js/components/Import/FromOtherPlugins/components/index.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/hooks/index.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/hooks/useImportSnippetSelection.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/hooks/useImporterSelection.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/hooks/useSnippetImport.ts
create mode 100644 src/js/components/Import/FromOtherPlugins/index.ts
create mode 100644 src/js/components/Import/ImportApp.tsx
create mode 100644 src/js/components/Import/shared/components/ImportCard.tsx
create mode 100644 src/js/components/Import/shared/components/ImportSection.tsx
create mode 100644 src/js/components/Import/shared/components/index.ts
create mode 100644 src/js/components/Import/shared/index.ts
create mode 100644 src/js/hooks/useAxios.ts
create mode 100644 src/js/hooks/useFileUploadAPI.ts
create mode 100644 src/js/hooks/useImportersAPI.ts
create mode 100644 src/js/import.tsx
create mode 100644 src/php/migration/importers/files/file-upload-importer.php
create mode 100644 src/php/migration/importers/plugins/header-footer-code-manager.php
create mode 100644 src/php/migration/importers/plugins/importer-base.php
create mode 100644 src/php/migration/importers/plugins/insert-headers-and-footers.php
create mode 100644 src/php/migration/importers/plugins/insert-php-code-snippet.php
create mode 100644 src/php/migration/importers/plugins/manager.php
diff --git a/config/webpack-js.ts b/config/webpack-js.ts
index b8a0d1fb..3e2a19ce 100644
--- a/config/webpack-js.ts
+++ b/config/webpack-js.ts
@@ -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`,
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
new file mode 100644
index 00000000..6369b89e
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -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('ignore')
+ const [currentStep, setCurrentStep] = useState('upload')
+ const selectSectionRef = useRef(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 (
+
+
+
{__('Upload one or more Code Snippets export files and the snippets will be imported.', 'code-snippets')}
+
+
+ {__('Afterward, you will need to visit the ', 'code-snippets')}
+
+ {__('All Snippets', 'code-snippets')}
+
+ {__(' page to activate the imported snippets.', 'code-snippets')}
+
+
+ {currentStep === 'upload' && (
+ <>
+
+ {(!importWorkflow.uploadResult || !importWorkflow.uploadResult.success) && (
+ <>
+
+
+
+ {__('Choose Files', 'code-snippets')}
+
+ {__('Choose one or more Code Snippets (.xml or .json) files to parse and preview.', 'code-snippets')}
+
+
+
+
+ {fileSelection.selectedFiles && fileSelection.selectedFiles.length > 0 && (
+
+ )}
+
+
+
+ {importWorkflow.isUploading
+ ? __('Uploading files...', 'code-snippets')
+ : __('Upload files', 'code-snippets')
+ }
+
+
+
+ >
+ )}
+ >
+ )}
+
+ {currentStep === 'select' && importWorkflow.availableSnippets.length > 0 && !importWorkflow.uploadResult?.success && (
+
+
+
+ {__('← Upload Different Files', 'code-snippets')}
+
+
+
+
+
{__('Available Snippets', 'code-snippets')} ({importWorkflow.availableSnippets.length})
+
+ {__('Select the snippets you want to import:', 'code-snippets')}
+
+
+
+
+ {snippetSelection.isAllSelected
+ ? __('Deselect All', 'code-snippets')
+ : __('Select All', 'code-snippets')
+ }
+
+
+ {importWorkflow.isImporting
+ ? __('Importing...', 'code-snippets')
+ : __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
+
+
+
+
+
+
+
+
+ {snippetSelection.isAllSelected
+ ? __('Deselect All', 'code-snippets')
+ : __('Select All', 'code-snippets')
+ }
+
+
+ {importWorkflow.isImporting
+ ? __('Importing...', 'code-snippets')
+ : __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
+
+
+
+ )}
+
+ {importWorkflow.uploadResult && (
+
+ )}
+
+
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
new file mode 100644
index 00000000..1c8b6029
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
@@ -0,0 +1,66 @@
+import React from 'react'
+import { __ } from '@wordpress/i18n'
+import { useDragAndDrop } from '../hooks/useDragAndDrop'
+
+interface DragDropUploadAreaProps {
+ fileInputRef: React.RefObject
+ onFileSelect: (files: FileList | null) => void
+ disabled?: boolean
+}
+
+export const DragDropUploadArea: React.FC = ({
+ fileInputRef,
+ onFileSelect,
+ disabled = false
+}) => {
+ const { dragOver, handleDragOver, handleDragLeave, handleDrop } = useDragAndDrop({
+ onFilesDrop: onFileSelect
+ })
+
+ const handleClick = () => {
+ if (!disabled) {
+ fileInputRef.current?.click()
+ }
+ }
+
+ return (
+ <>
+
+
📁
+
+ {__('Drag and drop files here, or click to browse', 'code-snippets')}
+
+
+ {__('Supports JSON and XML files', 'code-snippets')}
+
+
+
+ onFileSelect(e.target.files)}
+ style={{ display: 'none' }}
+ disabled={disabled}
+ />
+ >
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
new file mode 100644
index 00000000..b4757da0
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
@@ -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 = ({
+ value,
+ onChange
+}) => {
+ return (
+
+ {__('Duplicate Snippets', 'code-snippets')}
+
+ {__('What should happen if an existing snippet is found with an identical name to an imported snippet?', 'code-snippets')}
+
+
+
+
+
+ onChange(e.target.value as DuplicateAction)}
+ style={{ marginTop: '2px' }}
+ />
+
+ {__('Ignore any duplicate snippets: import all snippets from the file regardless and leave all existing snippets unchanged.', 'code-snippets')}
+
+
+
+
+ onChange(e.target.value as DuplicateAction)}
+ style={{ marginTop: '2px' }}
+ />
+
+ {__('Replace any existing snippets with a newly imported snippet of the same name.', 'code-snippets')}
+
+
+
+
+ onChange(e.target.value as DuplicateAction)}
+ style={{ marginTop: '2px' }}
+ />
+
+ {__('Do not import any duplicate snippets; leave all existing snippets unchanged.', 'code-snippets')}
+
+
+
+
+
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
new file mode 100644
index 00000000..92c762a9
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
@@ -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 = ({ result }) => {
+ return (
+
+
+
+
+ {result.success ? '✓' : '✕'}
+
+
+
+
+ {result.success
+ ? __('Import Successful!', 'code-snippets')
+ : __('Import Failed', 'code-snippets')
+ }
+
+
+ {result.message}
+
+
+ {result.success && (
+
+ {__('Go to ', 'code-snippets')}
+
+ {__('All Snippets', 'code-snippets')}
+
+ {__(' to activate your imported snippets.', 'code-snippets')}
+
+ )}
+
+ {result.warnings && result.warnings.length > 0 && (
+
+
+ {__('Warnings:', 'code-snippets')}
+
+
+ {result.warnings.map((warning, index) => (
+
+ {warning}
+
+ ))}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx b/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
new file mode 100644
index 00000000..3bf11aa3
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
@@ -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 = ({
+ files,
+ onRemoveFile
+}) => {
+ return (
+
+
+ {__('Selected Files:', 'code-snippets')} ({files.length})
+
+
+ {Array.from(files).map((file, index) => (
+
+
+
📄
+
+
{file.name}
+
+ {formatFileSize(file.size)}
+
+
+
+
{
+ e.stopPropagation()
+ onRemoveFile(index)
+ }}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: '#d63638',
+ cursor: 'pointer',
+ fontSize: '16px',
+ padding: '4px'
+ }}
+ title={__('Remove file', 'code-snippets')}
+ >
+ ✕
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx b/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
new file mode 100644
index 00000000..8a59d197
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+import { __ } from '@wordpress/i18n'
+import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
+
+interface SnippetSelectionTableProps {
+ snippets: ImportableSnippet[]
+ selectedSnippets: Set
+ isAllSelected: boolean
+ onSnippetToggle: (snippetId: number | string) => void
+ onSelectAll: () => void
+}
+
+export const SnippetSelectionTable: React.FC = ({
+ 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 (
+
+ )
+}
diff --git a/src/js/components/Import/FromFileUpload/components/index.ts b/src/js/components/Import/FromFileUpload/components/index.ts
new file mode 100644
index 00000000..d0581103
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/components/index.ts
@@ -0,0 +1,5 @@
+export { DuplicateActionSelector } from './DuplicateActionSelector'
+export { DragDropUploadArea } from './DragDropUploadArea'
+export { SelectedFilesList } from './SelectedFilesList'
+export { SnippetSelectionTable } from './SnippetSelectionTable'
+export { ImportResultDisplay } from './ImportResultDisplay'
diff --git a/src/js/components/Import/FromFileUpload/hooks/index.ts b/src/js/components/Import/FromFileUpload/hooks/index.ts
new file mode 100644
index 00000000..5826fc3e
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/hooks/index.ts
@@ -0,0 +1,4 @@
+export { useDragAndDrop } from './useDragAndDrop'
+export { useFileSelection } from './useFileSelection'
+export { useSnippetSelection } from './useSnippetSelection'
+export { useImportWorkflow } from './useImportWorkflow'
diff --git a/src/js/components/Import/FromFileUpload/hooks/useDragAndDrop.ts b/src/js/components/Import/FromFileUpload/hooks/useDragAndDrop.ts
new file mode 100644
index 00000000..f6c3b0bb
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/hooks/useDragAndDrop.ts
@@ -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
+ }
+}
diff --git a/src/js/components/Import/FromFileUpload/hooks/useFileSelection.ts b/src/js/components/Import/FromFileUpload/hooks/useFileSelection.ts
new file mode 100644
index 00000000..333fa452
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/hooks/useFileSelection.ts
@@ -0,0 +1,42 @@
+import { useState, useRef } from 'react'
+import { removeFileFromList } from '../utils/fileUtils'
+
+export const useFileSelection = () => {
+ const [selectedFiles, setSelectedFiles] = useState(null)
+ const fileInputRef = useRef(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
+ }
+}
diff --git a/src/js/components/Import/FromFileUpload/hooks/useImportWorkflow.ts b/src/js/components/Import/FromFileUpload/hooks/useImportWorkflow.ts
new file mode 100644
index 00000000..cbffe75e
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/hooks/useImportWorkflow.ts
@@ -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([])
+ const [uploadResult, setUploadResult] = useState(null)
+
+ const fileUploadAPI = useFileUploadAPI()
+
+ const parseFiles = async (files: FileList): Promise => {
+ 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 => {
+ 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
+ }
+}
diff --git a/src/js/components/Import/FromFileUpload/hooks/useSnippetSelection.ts b/src/js/components/Import/FromFileUpload/hooks/useSnippetSelection.ts
new file mode 100644
index 00000000..6d16cfe7
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/hooks/useSnippetSelection.ts
@@ -0,0 +1,45 @@
+import { useState } from 'react'
+import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
+
+export const useSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
+ const [selectedSnippets, setSelectedSnippets] = useState>(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
+ }
+}
diff --git a/src/js/components/Import/FromFileUpload/utils/fileUtils.ts b/src/js/components/Import/FromFileUpload/utils/fileUtils.ts
new file mode 100644
index 00000000..6ade360c
--- /dev/null
+++ b/src/js/components/Import/FromFileUpload/utils/fileUtils.ts
@@ -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
+}
diff --git a/src/js/components/Import/FromOtherPlugins/ImportForm.tsx b/src/js/components/Import/FromOtherPlugins/ImportForm.tsx
new file mode 100644
index 00000000..5c61cc50
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/ImportForm.tsx
@@ -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(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 (
+
+
{__('Loading importers...', 'code-snippets')}
+
+ )
+ }
+
+ if (importerSelection.error) {
+ return (
+
+
+
{__('Error loading importers:', 'code-snippets')} {importerSelection.error}
+
+
+ )
+ }
+
+ return (
+
+
+
{__('If you are using another Snippets plugin, you can import all existing snippets to your Code Snippets library.', 'code-snippets')}
+
+
+
+ {snippetImport.snippetsError && (
+
+ )}
+
+ {snippetImport.importError && (
+
+ )}
+
+ {snippetImport.importSuccess.length > 0 && (
+
+ )}
+
+ {importerSelection.selectedImporter &&
+ !snippetImport.isLoadingSnippets &&
+ !snippetImport.snippetsError &&
+ snippetImport.snippets.length === 0 &&
+ snippetImport.importSuccess.length === 0 && (
+
+
+
📭
+
+ {__('No snippets found', 'code-snippets')}
+
+
+ {__('No snippets were found for the selected plugin. Make sure the plugin is installed and has snippets configured.', 'code-snippets')}
+
+
+
+ )}
+
+ {snippetImport.snippets.length > 0 && (
+ <>
+
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
new file mode 100644
index 00000000..ac084959
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
@@ -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 = ({
+ autoAddTags,
+ tagValue,
+ onAutoAddTagsChange,
+ onTagValueChange
+}) => {
+ return (
+
+ {__('Import options', 'code-snippets')}
+
+ onAutoAddTagsChange(e.target.checked)}
+ style={{ marginTop: '2px' }}
+ />
+
+
+ {__('Automatically add Tag', 'code-snippets')}
+
+
+ {__('For your convenience, we can add a tag on every imported snippet.', 'code-snippets')}
+
+
+ {autoAddTags && (
+
+ onTagValueChange(e.target.value)}
+ placeholder={__('Add tag...', 'code-snippets')}
+ className="regular-text"
+ style={{ width: '100%', maxWidth: '300px' }}
+ />
+
+ )}
+
+
+
+ )
+}
diff --git a/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx b/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
new file mode 100644
index 00000000..bed4d287
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
@@ -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 = ({
+ importers,
+ selectedImporter,
+ onImporterChange,
+ isLoading
+}) => {
+ return (
+
+
+ {__('Select Plugin', 'code-snippets')}
+
+ onImporterChange(event.target.value)}
+ className="regular-text"
+ style={{ display: 'block', marginTop: '5px', width: '100%', maxWidth: '300px' }}
+ disabled={isLoading}
+ >
+ {__('-- Select an importer --', 'code-snippets')}
+ {importers.map(importer => (
+
+ {importer.title} {!importer.is_active ? __('(Inactive)', 'code-snippets') : ''}
+
+ ))}
+
+ {isLoading && (
+
+ {__('Loading snippets...', 'code-snippets')}
+
+ )}
+
+ )
+}
diff --git a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
new file mode 100644
index 00000000..f6c7f8ec
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
@@ -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
+ onSnippetToggle: (snippetId: number) => void
+ onSelectAll: () => void
+ onImport: () => void
+ isImporting: boolean
+}
+
+export const SimpleSnippetTable: React.FC = ({
+ snippets,
+ selectedSnippets,
+ onSnippetToggle,
+ onSelectAll,
+ onImport,
+ isImporting
+}) => {
+ const isAllSelected = selectedSnippets.size === snippets.length && snippets.length > 0
+
+ return (
+
+
+
+
{__('Available Snippets', 'code-snippets')} ({snippets.length})
+
{__('We found the following snippets.', 'code-snippets')}
+
+
+
+ {isAllSelected
+ ? __('Deselect All', 'code-snippets')
+ : __('Select All', 'code-snippets')
+ }
+
+
+ {isImporting
+ ? __('Importing...', 'code-snippets')
+ : __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
+
+
+
+
+
+
+
+
+ {isAllSelected
+ ? __('Deselect All', 'code-snippets')
+ : __('Select All', 'code-snippets')
+ }
+
+
+ {isImporting
+ ? __('Importing...', 'code-snippets')
+ : __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
+
+
+
+ )
+}
diff --git a/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
new file mode 100644
index 00000000..09e0b39a
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
@@ -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 = ({
+ type,
+ title,
+ message,
+ showSnippetsLink = false
+}) => {
+ const isError = type === 'error'
+
+ return (
+
+
+
+ {isError ? '✕' : '✓'}
+
+
+
+
+ )
+}
diff --git a/src/js/components/Import/FromOtherPlugins/components/index.ts b/src/js/components/Import/FromOtherPlugins/components/index.ts
new file mode 100644
index 00000000..3ab18c68
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/components/index.ts
@@ -0,0 +1,4 @@
+export { ImporterSelector } from './ImporterSelector'
+export { ImportOptions } from './ImportOptions'
+export { SimpleSnippetTable } from './SimpleSnippetTable'
+export { StatusDisplay } from './StatusDisplay'
diff --git a/src/js/components/Import/FromOtherPlugins/hooks/index.ts b/src/js/components/Import/FromOtherPlugins/hooks/index.ts
new file mode 100644
index 00000000..e60c8632
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/hooks/index.ts
@@ -0,0 +1,3 @@
+export { useImporterSelection } from './useImporterSelection'
+export { useSnippetImport } from './useSnippetImport'
+export { useImportSnippetSelection } from './useImportSnippetSelection'
diff --git a/src/js/components/Import/FromOtherPlugins/hooks/useImportSnippetSelection.ts b/src/js/components/Import/FromOtherPlugins/hooks/useImportSnippetSelection.ts
new file mode 100644
index 00000000..298f6a6a
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/hooks/useImportSnippetSelection.ts
@@ -0,0 +1,45 @@
+import { useState } from 'react'
+import type { ImportableSnippet } from '../../../../hooks/useImportersAPI'
+
+export const useImportSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
+ const [selectedSnippets, setSelectedSnippets] = useState>(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
+ }
+}
diff --git a/src/js/components/Import/FromOtherPlugins/hooks/useImporterSelection.ts b/src/js/components/Import/FromOtherPlugins/hooks/useImporterSelection.ts
new file mode 100644
index 00000000..71080a3a
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/hooks/useImporterSelection.ts
@@ -0,0 +1,42 @@
+import { useState, useEffect } from 'react'
+import { useImportersAPI, type Importer } from '../../../../hooks/useImportersAPI'
+
+export const useImporterSelection = () => {
+ const [importers, setImporters] = useState([])
+ const [selectedImporter, setSelectedImporter] = useState('')
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [tagValue, setTagValue] = useState('')
+
+ 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
+ }
+}
diff --git a/src/js/components/Import/FromOtherPlugins/hooks/useSnippetImport.ts b/src/js/components/Import/FromOtherPlugins/hooks/useSnippetImport.ts
new file mode 100644
index 00000000..a20a60b1
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/hooks/useSnippetImport.ts
@@ -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([])
+ const [isLoadingSnippets, setIsLoadingSnippets] = useState(false)
+ const [snippetsError, setSnippetsError] = useState(null)
+ const [isImporting, setIsImporting] = useState(false)
+ const [importError, setImportError] = useState(null)
+ const [importSuccess, setImportSuccess] = useState([])
+
+ const importersAPI = useImportersAPI()
+
+ const loadSnippets = async (importerName: string): Promise => {
+ 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 => {
+ 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
+ }
+}
diff --git a/src/js/components/Import/FromOtherPlugins/index.ts b/src/js/components/Import/FromOtherPlugins/index.ts
new file mode 100644
index 00000000..977c810c
--- /dev/null
+++ b/src/js/components/Import/FromOtherPlugins/index.ts
@@ -0,0 +1 @@
+export * from './ImportForm'
diff --git a/src/js/components/Import/ImportApp.tsx b/src/js/components/Import/ImportApp.tsx
new file mode 100644
index 00000000..2d036bdd
--- /dev/null
+++ b/src/js/components/Import/ImportApp.tsx
@@ -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('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 (
+
+ )
+}
diff --git a/src/js/components/Import/shared/components/ImportCard.tsx b/src/js/components/Import/shared/components/ImportCard.tsx
new file mode 100644
index 00000000..64d8182f
--- /dev/null
+++ b/src/js/components/Import/shared/components/ImportCard.tsx
@@ -0,0 +1,45 @@
+import React from 'react'
+import classnames from 'classnames'
+import type { HTMLAttributes } from 'react'
+
+export interface ImportCardProps extends Omit, 'className'> {
+ children: React.ReactNode
+ className?: string
+ variant?: 'default' | 'controls'
+}
+
+export const ImportCard = React.forwardRef(({
+ 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 (
+
+ {children}
+
+ )
+})
+
+ImportCard.displayName = 'ImportCard'
diff --git a/src/js/components/Import/shared/components/ImportSection.tsx b/src/js/components/Import/shared/components/ImportSection.tsx
new file mode 100644
index 00000000..262798c9
--- /dev/null
+++ b/src/js/components/Import/shared/components/ImportSection.tsx
@@ -0,0 +1,33 @@
+import React from 'react'
+import type { HTMLAttributes } from 'react'
+
+export interface ImportSectionProps extends Omit, 'style'> {
+ children: React.ReactNode
+ active?: boolean
+ className?: string
+ style?: React.CSSProperties
+}
+
+export const ImportSection: React.FC = ({
+ children,
+ active = false,
+ className,
+ style,
+ ...props
+}) => {
+ const sectionStyle: React.CSSProperties = {
+ display: active ? 'block' : 'none',
+ paddingTop: 0,
+ ...style
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/js/components/Import/shared/components/index.ts b/src/js/components/Import/shared/components/index.ts
new file mode 100644
index 00000000..c59b6b1c
--- /dev/null
+++ b/src/js/components/Import/shared/components/index.ts
@@ -0,0 +1,4 @@
+export { ImportCard } from './ImportCard'
+export type { ImportCardProps } from './ImportCard'
+export { ImportSection } from './ImportSection'
+export type { ImportSectionProps } from './ImportSection'
diff --git a/src/js/components/Import/shared/index.ts b/src/js/components/Import/shared/index.ts
new file mode 100644
index 00000000..cb64ac1b
--- /dev/null
+++ b/src/js/components/Import/shared/index.ts
@@ -0,0 +1 @@
+export * from './components'
diff --git a/src/js/hooks/useAxios.ts b/src/js/hooks/useAxios.ts
new file mode 100644
index 00000000..a513e6ab
--- /dev/null
+++ b/src/js/hooks/useAxios.ts
@@ -0,0 +1,39 @@
+import { useMemo } from 'react'
+import axios from 'axios'
+import type { AxiosInstance, AxiosResponse, CreateAxiosDefaults } from 'axios'
+
+export interface AxiosAPI {
+ get: (url: string) => Promise>
+ post: (url: string, data?: D) => Promise>
+ del: (url: string) => Promise>
+ axiosInstance: AxiosInstance
+}
+
+const debugRequest = async (
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
+ url: string,
+ doRequest: Promise>,
+ data?: D
+): Promise> => {
+ console.debug(`${method} ${url}`, ...data ? [data] : [])
+ const response = await doRequest
+ console.debug('Response', response)
+ return response
+}
+
+export const useAxios = (defaultConfig: CreateAxiosDefaults): AxiosAPI => {
+ const axiosInstance = useMemo(() => axios.create(defaultConfig), [defaultConfig])
+
+ return useMemo((): AxiosAPI => ({
+ get: (url: string): Promise> =>
+ debugRequest('GET', url, axiosInstance.get, never>(url)),
+
+ post: (url: string, data?: D) =>
+ debugRequest('POST', url, axiosInstance.post, D>(url, data), data),
+
+ del: (url: string) =>
+ debugRequest('DELETE', url, axiosInstance.delete, never>(url)),
+
+ axiosInstance
+ }), [axiosInstance])
+}
diff --git a/src/js/hooks/useFileUploadAPI.ts b/src/js/hooks/useFileUploadAPI.ts
new file mode 100644
index 00000000..14c6ba06
--- /dev/null
+++ b/src/js/hooks/useFileUploadAPI.ts
@@ -0,0 +1,87 @@
+import { useMemo } from 'react'
+import { useAxios } from './useAxios'
+import type { AxiosResponse, CreateAxiosDefaults } from 'axios'
+
+export interface FileUploadRequest {
+ files: FileList
+}
+
+export interface FileParseResponse {
+ snippets: ImportableSnippet[]
+ total_count: number
+ message: string
+ warnings?: string[]
+}
+
+export interface ImportableSnippet {
+ id?: number
+ name: string
+ desc?: string
+ description?: string
+ code: string
+ tags?: string[]
+ scope?: string
+ source_file?: string
+ table_data: {
+ id: number | string
+ title: string
+ scope: string
+ tags: string
+ description: string
+ type: string
+ }
+}
+
+export interface SnippetImportRequest {
+ snippets: ImportableSnippet[]
+ duplicate_action: 'ignore' | 'replace' | 'skip'
+ network?: boolean
+}
+
+export interface SnippetImportResponse {
+ imported: number
+ imported_ids: number[]
+ message: string
+}
+
+const ROUTE_BASE = `${window.CODE_SNIPPETS?.restAPI.base}code-snippets/v1/`
+
+const AXIOS_CONFIG: CreateAxiosDefaults = {
+ headers: { 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce }
+}
+
+export interface FileUploadAPI {
+ parseFiles: (request: FileUploadRequest) => Promise>
+ importSnippets: (request: SnippetImportRequest) => Promise>
+}
+
+export const useFileUploadAPI = (): FileUploadAPI => {
+ const { axiosInstance } = useAxios(AXIOS_CONFIG)
+
+ return useMemo((): FileUploadAPI => ({
+ parseFiles: (request: FileUploadRequest) => {
+ const formData = new FormData()
+
+ for (let i = 0; i < request.files.length; i++) {
+ formData.append('files[]', request.files[i])
+ }
+
+ return axiosInstance.post(
+ `${ROUTE_BASE}file-upload/parse`,
+ formData,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ }
+ }
+ )
+ },
+
+ importSnippets: (request: SnippetImportRequest) => {
+ return axiosInstance.post(
+ `${ROUTE_BASE}file-upload/import`,
+ request
+ )
+ }
+ }), [axiosInstance])
+}
diff --git a/src/js/hooks/useImportersAPI.ts b/src/js/hooks/useImportersAPI.ts
new file mode 100644
index 00000000..cdfc723b
--- /dev/null
+++ b/src/js/hooks/useImportersAPI.ts
@@ -0,0 +1,52 @@
+import { useMemo } from 'react'
+import { useAxios } from './useAxios'
+import type { AxiosResponse, CreateAxiosDefaults } from 'axios'
+
+export interface Importer {
+ name: string
+ title: string
+ is_active: boolean
+}
+
+export interface ImportableSnippet {
+ id: number
+ title: string
+ table_data: {
+ id: number
+ title: string
+ }
+}
+
+export interface ImportRequest {
+ ids: number[]
+ network?: boolean
+ auto_add_tags?: boolean
+ tag_value?: string
+}
+
+export interface ImportResponse {
+ imported: number[]
+}
+
+const ROUTE_BASE = `${window.CODE_SNIPPETS?.restAPI.base}code-snippets/v1/`
+
+const AXIOS_CONFIG: CreateAxiosDefaults = {
+ headers: { 'X-WP-Nonce': window.CODE_SNIPPETS?.restAPI.nonce }
+}
+
+export interface ImportersAPI {
+ fetchAll: () => Promise>
+ fetchSnippets: (importerName: string) => Promise>
+ importSnippets: (importerName: string, request: ImportRequest) => Promise>
+}
+
+export const useImportersAPI = (): ImportersAPI => {
+ const { get, post } = useAxios(AXIOS_CONFIG)
+
+ return useMemo((): ImportersAPI => ({
+ fetchAll: () => get(`${ROUTE_BASE}importers`),
+ fetchSnippets: (importerName: string) => get(`${ROUTE_BASE}${importerName}`),
+ importSnippets: (importerName: string, request: ImportRequest) =>
+ post(`${ROUTE_BASE}${importerName}/import`, request)
+ }), [get, post])
+}
diff --git a/src/js/import.tsx b/src/js/import.tsx
new file mode 100644
index 00000000..b1c9b824
--- /dev/null
+++ b/src/js/import.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import { createRoot } from 'react-dom/client'
+import { ImportApp } from './components/Import/ImportApp'
+
+const importContainer = document.getElementById('import-container')
+
+if (importContainer) {
+ const root = createRoot(importContainer)
+ root.render( )
+} else {
+ console.error('Could not find import container.')
+}
diff --git a/src/php/admin-menus/class-import-menu.php b/src/php/admin-menus/class-import-menu.php
index 397d9de2..0f6674ff 100644
--- a/src/php/admin-menus/class-import-menu.php
+++ b/src/php/admin-menus/class-import-menu.php
@@ -38,64 +38,6 @@ public function load() {
$contextual_help = new Contextual_Help( 'import' );
$contextual_help->load();
-
- $this->process_import_files();
- }
-
- /**
- * Process the uploaded import files
- */
- private function process_import_files() {
-
- // Ensure the import file exists.
- if ( ! isset(
- $_FILES['code_snippets_import_files']['name'],
- $_FILES['code_snippets_import_files']['type'],
- $_FILES['code_snippets_import_files']['tmp_name']
- ) ) {
- return;
- }
-
- check_admin_referer( 'import_code_snippets_file' );
-
- // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
- $upload_files = $_FILES['code_snippets_import_files']['tmp_name'];
- // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
- $upload_filenames = $_FILES['code_snippets_import_files']['name'];
- $upload_mime_types = array_map( 'sanitize_mime_type', wp_unslash( $_FILES['code_snippets_import_files']['type'] ) );
-
- $count = 0;
- $network = is_network_admin();
- $error = false;
- $dup_action = isset( $_POST['duplicate_action'] ) ? sanitize_key( $_POST['duplicate_action'] ) : 'ignore';
-
- // Loop through the uploaded files and import the snippets.
- foreach ( $upload_files as $i => $import_file ) {
- $filename_info = pathinfo( $upload_filenames[ $i ] );
- $ext = $filename_info['extension'];
- $mime_type = $upload_mime_types[ $i ];
-
- $import = new Import( $import_file, $network, $dup_action );
-
- if ( 'json' === $ext || 'application/json' === $mime_type ) {
- $result = $import->import_json();
- } elseif ( 'xml' === $ext || 'text/xml' === $mime_type ) {
- $result = $import->import_xml();
- } else {
- $result = false;
- }
-
- if ( false === $result ) {
- $error = true;
- } else {
- $count += count( $result );
- }
- }
-
- // Send the amount of imported snippets to the page.
- $url = add_query_arg( $error ? array( 'error' => true ) : array( 'imported' => $count ) );
- wp_safe_redirect( esc_url_raw( $url ) );
- exit;
}
/**
@@ -118,53 +60,26 @@ public function register_importer() {
}
/**
- * Print the status and error messages
- */
- protected function print_messages() {
-
- if ( ! empty( $_REQUEST['error'] ) ) {
- echo '';
- esc_html_e( 'An error occurred when processing the import files.', 'code-snippets' );
- echo '
';
- }
-
- if ( isset( $_REQUEST['imported'] ) ) {
- echo '';
-
- $imported = intval( $_REQUEST['imported'] );
-
- if ( 0 === $imported ) {
- esc_html_e( 'No snippets were imported.', 'code-snippets' );
-
- } else {
- printf(
- // translators: %d: amount of snippets imported.
- _n(
- 'Successfully imported %d snippet.',
- 'Successfully imported %d snippets.',
- $imported,
- 'code-snippets'
- ),
- '' . number_format_i18n( $imported ) . ' ',
- );
-
- printf(
- ' %s ',
- esc_url( code_snippets()->get_menu_url( 'manage' ) ),
- esc_html__( 'Have fun!', 'code-snippets' )
- );
- }
-
- echo '
';
- }
- }
-
- /**
- * Empty implementation for enqueue_assets.
+ * Enqueue assets for the import menu.
*
* @return void
*/
public function enqueue_assets() {
- // none required.
+ $plugin = code_snippets();
+
+ wp_enqueue_script(
+ 'code-snippets-import',
+ plugins_url( 'dist/import.js', $plugin->file ),
+ [
+ 'react',
+ 'react-dom',
+ 'wp-i18n',
+ 'wp-components',
+ ],
+ $plugin->version,
+ true
+ );
+
+ $plugin->localize_script( 'code-snippets-import' );
}
}
diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php
index c05613e2..bb76103e 100644
--- a/src/php/class-plugin.php
+++ b/src/php/class-plugin.php
@@ -132,7 +132,7 @@ public function load_plugin() {
// Settings component.
require_once $includes_path . '/settings/settings-fields.php';
require_once $includes_path . '/settings/editor-preview.php';
- require_once $includes_path . '/settings/class-version-switch.php';
+ require_once $includes_path . '/settings/class-version-switch.php';
require_once $includes_path . '/settings/settings.php';
// Cloud List Table shared functions.
@@ -156,6 +156,10 @@ public function load_plugin() {
$upgrade = new Upgrade( $this->version, $this->db );
add_action( 'plugins_loaded', array( $upgrade, 'run' ), 0 );
$this->licensing = new Licensing();
+
+ // Importers.
+ new Plugins_Import_Manager();
+ new Files_Import_Manager();
}
/**
diff --git a/src/php/migration/importers/files/file-upload-importer.php b/src/php/migration/importers/files/file-upload-importer.php
new file mode 100644
index 00000000..e4ec32f9
--- /dev/null
+++ b/src/php/migration/importers/files/file-upload-importer.php
@@ -0,0 +1,406 @@
+ WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'parse_uploaded_files' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ ] );
+
+ register_rest_route( $namespace, 'file-upload/import', [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'import_selected_snippets' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ 'args' => [
+ 'snippets' => [
+ 'description' => __( 'Array of snippet data to import', 'code-snippets' ),
+ 'type' => 'array',
+ 'required' => true,
+ ],
+ 'duplicate_action' => [
+ 'description' => __( 'Action to take when duplicate snippets are found', 'code-snippets' ),
+ 'type' => 'string',
+ 'enum' => [ 'ignore', 'replace', 'skip' ],
+ 'default' => 'ignore',
+ ],
+ 'network' => [
+ 'description' => __( 'Whether to import to network table', 'code-snippets' ),
+ 'type' => 'boolean',
+ 'default' => false,
+ ],
+ ],
+ ] );
+ }
+
+ public function parse_uploaded_files( WP_REST_Request $request ) {
+ // Verify nonce for security
+ $nonce = $request->get_header( 'X-WP-Nonce' );
+ if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
+ return new WP_Error(
+ 'rest_cookie_invalid_nonce',
+ __( 'Cookie check failed', 'code-snippets' ),
+ [ 'status' => 403 ]
+ );
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above via REST API header
+ if ( empty( $_FILES['files'] ) ) {
+ return new WP_Error(
+ 'no_files',
+ __( 'No files were uploaded.', 'code-snippets' ),
+ [ 'status' => 400 ]
+ );
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above, file data validated below
+ $files = $_FILES['files'];
+
+ if ( ! isset( $files['name'], $files['type'], $files['tmp_name'], $files['error'] ) ) {
+ return new WP_Error(
+ 'invalid_file_data',
+ __( 'Invalid file upload data.', 'code-snippets' ),
+ [ 'status' => 400 ]
+ );
+ }
+
+ $all_snippets = [];
+ $errors = [];
+
+ $file_count = is_array( $files['name'] ) ? count( $files['name'] ) : 1;
+
+ for ( $i = 0; $i < $file_count; $i++ ) {
+ $file_name = is_array( $files['name'] ) ? $files['name'][ $i ] : $files['name'];
+ $file_type = is_array( $files['type'] ) ? $files['type'][ $i ] : $files['type'];
+ $file_tmp = is_array( $files['tmp_name'] ) ? $files['tmp_name'][ $i ] : $files['tmp_name'];
+ $file_error = is_array( $files['error'] ) ? $files['error'][ $i ] : $files['error'];
+
+ if ( UPLOAD_ERR_OK !== $file_error ) {
+ $errors[] = sprintf(
+ /* translators: %1$s: file name, %2$s: error message */
+ __( 'Upload error for file %1$s: %2$s', 'code-snippets' ),
+ $file_name,
+ $this->get_upload_error_message( $file_error )
+ );
+ continue;
+ }
+
+ $file_info = pathinfo( $file_name );
+ $extension = strtolower( $file_info['extension'] ?? '' );
+ $mime_type = sanitize_mime_type( $file_type );
+
+ if ( ! $this->is_valid_file_type( $extension, $mime_type ) ) {
+ $errors[] = sprintf(
+ /* translators: %s: file name */
+ __( 'Invalid file type for %s. Only JSON and XML files are allowed.', 'code-snippets' ),
+ $file_name,
+ );
+ continue;
+ }
+
+ $snippets = $this->parse_file_content( $file_tmp, $extension, $mime_type, $file_name );
+
+ if ( is_wp_error( $snippets ) ) {
+ $errors[] = sprintf(
+ /* translators: %1$s: file name, %2$s: error message */
+ __( 'Error parsing %1$s: %2$s', 'code-snippets' ),
+ $file_name,
+ $snippets->get_error_message(),
+ );
+ } else {
+ $all_snippets = array_merge( $all_snippets, $snippets );
+ }
+ }
+
+ if ( empty( $all_snippets ) ) {
+ return new WP_Error(
+ 'no_snippets_found',
+ __( 'No valid snippets found in the uploaded files.', 'code-snippets' ),
+ [
+ 'status' => 400,
+ 'errors' => $errors,
+ ],
+ );
+ }
+
+ $response = [
+ 'snippets' => $all_snippets,
+ 'total_count' => count( $all_snippets ),
+ 'message' => sprintf(
+ /* translators: %d: number of snippets */
+ _n(
+ 'Found %d snippet ready for import.',
+ 'Found %d snippets ready for import.',
+ count( $all_snippets ),
+ 'code-snippets',
+ ),
+ count( $all_snippets )
+ ),
+ ];
+
+ if ( ! empty( $errors ) ) {
+ $response['warnings'] = $errors;
+ }
+
+ return rest_ensure_response( $response );
+ }
+
+ public function import_selected_snippets( WP_REST_Request $request ) {
+ $snippets_data = $request->get_param( 'snippets' );
+ $duplicate_action = $request->get_param( 'duplicate_action' ) ?? 'ignore';
+ $network = $request->get_param( 'network' ) ?? false;
+
+ if ( empty( $snippets_data ) || ! is_array( $snippets_data ) ) {
+ return new WP_Error(
+ 'no_snippets',
+ __( 'No snippet data provided for import.', 'code-snippets' ),
+ [ 'status' => 400 ]
+ );
+ }
+
+ $snippets = [];
+ foreach ( $snippets_data as $snippet_data ) {
+ $snippet = new Snippet();
+ $snippet->network = $network;
+
+ $import_fields = [
+ 'name',
+ 'desc',
+ 'description',
+ 'code',
+ 'tags',
+ 'scope',
+ 'priority',
+ 'shared_network',
+ 'modified',
+ 'cloud_id',
+ ];
+
+ foreach ( $import_fields as $field ) {
+ if ( isset( $snippet_data[ $field ] ) ) {
+ $snippet->set_field( $field, $snippet_data[ $field ] );
+ }
+ }
+
+ $snippets[] = $snippet;
+ }
+
+ $imported = $this->save_snippets( $snippets, $duplicate_action, $network );
+
+ $response = [
+ 'imported' => count( $imported ),
+ 'imported_ids' => $imported,
+ 'message' => sprintf(
+ /* translators: %d: number of snippets */
+ _n(
+ 'Successfully imported %d snippet.',
+ 'Successfully imported %d snippets.',
+ count( $imported ),
+ 'code-snippets',
+ ),
+ count( $imported )
+ ),
+ ];
+
+ return rest_ensure_response( $response );
+ }
+
+ private function parse_file_content( string $file_path, string $extension, string $mime_type, string $file_name ) {
+ if ( ! file_exists( $file_path ) || ! is_file( $file_path ) ) {
+ return new WP_Error(
+ 'file_not_found',
+ __( 'File not found or is not a valid file.', 'code-snippets' )
+ );
+ }
+
+ if ( 'json' === $extension || 'application/json' === $mime_type ) {
+ return $this->parse_json_file( $file_path, $file_name );
+ } elseif ( 'xml' === $extension || in_array( $mime_type, [ 'text/xml', 'application/xml' ], true ) ) {
+ return $this->parse_xml_file( $file_path, $file_name );
+ }
+
+ return new WP_Error(
+ 'unsupported_file_type',
+ __( 'Unsupported file type.', 'code-snippets' )
+ );
+ }
+
+ private function parse_json_file( string $file_path, string $file_name ) {
+ $raw_data = file_get_contents( $file_path );
+ $data = json_decode( $raw_data, true );
+
+ if ( json_last_error() !== JSON_ERROR_NONE ) {
+ return new WP_Error(
+ 'invalid_json',
+ sprintf(
+ /* translators: %1$s: file name, %2$s: error message */
+ __( 'Invalid JSON in file %1$s: %2$s', 'code-snippets' ),
+ $file_name,
+ json_last_error_msg()
+ )
+ );
+ }
+
+ if ( ! isset( $data['snippets'] ) || ! is_array( $data['snippets'] ) ) {
+ return new WP_Error(
+ 'no_snippets_in_file',
+ sprintf(
+ /* translators: %s: file name */
+ __( 'No snippets found in file %s', 'code-snippets' ),
+ $file_name
+ )
+ );
+ }
+
+ $snippets = [];
+ foreach ( $data['snippets'] as $snippet_data ) {
+ $snippet_data['source_file'] = $file_name;
+
+ $snippet_data['table_data'] = [
+ 'id' => $snippet_data['id'] ?? uniqid(),
+ 'title' => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
+ 'scope' => $snippet_data['scope'] ?? 'global',
+ 'tags' => is_array( $snippet_data['tags'] ?? [] ) ? implode( ', ', $snippet_data['tags'] ) : '',
+ 'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
+ 'type' => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' )
+ ];
+
+ $snippets[] = $snippet_data;
+ }
+
+ return $snippets;
+ }
+
+ private function parse_xml_file( string $file_path, string $file_name ) {
+ $dom = new \DOMDocument( '1.0', get_bloginfo( 'charset' ) );
+
+ if ( ! $dom->load( $file_path ) ) {
+ return new WP_Error(
+ 'invalid_xml',
+ sprintf(
+ /* translators: %s: file name */
+ __( 'Invalid XML in file %s', 'code-snippets' ),
+ $file_name
+ )
+ );
+ }
+
+ $snippets_xml = $dom->getElementsByTagName( 'snippet' );
+ $fields = [ 'name', 'description', 'desc', 'code', 'tags', 'scope' ];
+
+ $snippets = [];
+ $index = 0;
+
+ foreach ( $snippets_xml as $snippet_xml ) {
+ $snippet_data = [];
+
+ foreach ( $fields as $field_name ) {
+ $field = $snippet_xml->getElementsByTagName( $field_name )->item( 0 );
+
+ if ( isset( $field->nodeValue ) ) {
+ $snippet_data[ $field_name ] = $field->nodeValue;
+ }
+ }
+
+ $scope = $snippet_xml->getAttribute( 'scope' );
+ if ( ! empty( $scope ) ) {
+ $snippet_data['scope'] = $scope;
+ }
+
+ $snippet_data['source_file'] = $file_name;
+
+ $snippet_data['table_data'] = [
+ 'id' => ++$index,
+ 'title' => $snippet_data['name'] ?? __( 'Untitled Snippet', 'code-snippets' ),
+ 'scope' => $snippet_data['scope'] ?? 'global',
+ 'tags' => $snippet_data['tags'] ?? '',
+ 'description' => $snippet_data['desc'] ?? $snippet_data['description'] ?? '',
+ 'type' => Snippet::get_type_from_scope( $snippet_data['scope'] ?? 'global' ),
+ ];
+
+ $snippets[] = $snippet_data;
+ }
+
+ return $snippets;
+ }
+
+ private function save_snippets( array $snippets, string $duplicate_action, bool $network ): array {
+ $existing_snippets = [];
+
+ if ( 'replace' === $duplicate_action || 'skip' === $duplicate_action ) {
+ $all_snippets = get_snippets( [], $network );
+
+ foreach ( $all_snippets as $snippet ) {
+ if ( $snippet->name ) {
+ $existing_snippets[ $snippet->name ] = $snippet->id;
+ }
+ }
+ }
+
+ $imported = [];
+
+ foreach ( $snippets as $snippet ) {
+ if ( 'ignore' !== $duplicate_action && isset( $existing_snippets[ $snippet->name ] ) ) {
+ if ( 'replace' === $duplicate_action ) {
+ $snippet->id = $existing_snippets[ $snippet->name ];
+ } elseif ( 'skip' === $duplicate_action ) {
+ continue;
+ }
+ }
+
+ $saved_snippet = save_snippet( $snippet );
+
+ $snippet_id = $saved_snippet->id;
+
+ if ( $snippet_id ) {
+ $imported[] = $snippet_id;
+ }
+ }
+
+ return $imported;
+ }
+
+ private function is_valid_file_type( string $extension, string $mime_type ): bool {
+ $valid_extensions = [ 'json', 'xml' ];
+ $valid_mime_types = [ 'application/json', 'text/xml', 'application/xml' ];
+
+ return in_array( $extension, $valid_extensions, true ) ||
+ in_array( $mime_type, $valid_mime_types, true );
+ }
+
+
+
+ private function get_upload_error_message( int $error_code ): string {
+ $error_messages = [
+ UPLOAD_ERR_INI_SIZE => __( 'File exceeds the upload_max_filesize directive.', 'code-snippets' ),
+ UPLOAD_ERR_FORM_SIZE => __( 'File exceeds the MAX_FILE_SIZE directive.', 'code-snippets' ),
+ UPLOAD_ERR_PARTIAL => __( 'File was only partially uploaded.', 'code-snippets' ),
+ UPLOAD_ERR_NO_FILE => __( 'No file was uploaded.', 'code-snippets' ),
+ UPLOAD_ERR_NO_TMP_DIR => __( 'Missing a temporary folder.', 'code-snippets' ),
+ UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'code-snippets' ),
+ UPLOAD_ERR_EXTENSION => __( 'A PHP extension stopped the file upload.', 'code-snippets' ),
+ ];
+
+ return $error_messages[ $error_code ] ?? __( 'Unknown upload error.', 'code-snippets' );
+ }
+}
diff --git a/src/php/migration/importers/plugins/header-footer-code-manager.php b/src/php/migration/importers/plugins/header-footer-code-manager.php
new file mode 100644
index 00000000..8503a142
--- /dev/null
+++ b/src/php/migration/importers/plugins/header-footer-code-manager.php
@@ -0,0 +1,149 @@
+ 'name',
+ 'snippet' => 'code',
+ 'location' => 'scope',
+ 'created' => 'modified',
+ ];
+
+ private const HTML_SCOPE_TRANSFORMATIONS = [
+ '' => 'content',
+ 'header' => 'head-content',
+ 'footer' => 'footer-content',
+ ];
+
+ public function get_name() {
+ return 'header-footer-code-manager';
+ }
+
+ public function get_title() {
+ return esc_html__( 'Header Footer Code Manager', 'code-snippets' );
+ }
+
+ public static function is_active(): bool {
+ return is_plugin_active( 'header-footer-code-manager/99robots-header-footer-code-manager.php' );
+ }
+
+ public function get_data( array $ids_to_import = [] ) {
+ global $wpdb;
+ $nnr_hfcm_table_name = $wpdb->prefix . 'hfcm_scripts';
+ $sql = "SELECT * FROM `{$nnr_hfcm_table_name}`";
+
+ if ( ! empty( $ids_to_import ) ) {
+ $sql .= ' WHERE script_id IN (' . implode( ',', $ids_to_import ) . ')';
+ }
+
+ $snippets = $wpdb->get_results(
+ $sql
+ );
+
+ foreach ( $snippets as $snippet ) {
+ $snippet->table_data = [
+ 'id' => (int) $snippet->script_id,
+ 'title' => $snippet->name,
+ ];
+ }
+
+ return $snippets;
+ }
+
+ public function create_snippet( $snippet_data, bool $multisite ): ?Snippet {
+ $code_type = $snippet_data->snippet_type ?? '';
+
+ $snippet = new Snippet();
+ $snippet->network = $multisite;
+
+ foreach ( self::FIELD_MAPPINGS as $source_field => $target_field ) {
+ if ( ! isset( $snippet_data->$source_field ) ) {
+ continue;
+ }
+
+ $value = $this->transform_field_value(
+ $target_field,
+ $snippet_data->$source_field,
+ $snippet_data
+ );
+
+ $scope_not_supported = 'scope' === $target_field && null === $value;
+ if ( $scope_not_supported ) {
+ return null;
+ }
+
+ $snippet->set_field( $target_field, $value );
+ }
+
+ return $snippet;
+ }
+
+ private function transform_field_value( string $target_field, $value, $snippet_data ) {
+ if ( 'scope' === $target_field ) {
+ return $this->transform_scope_value( $value, $snippet_data );
+ }
+
+ if ( 'code' === $target_field ) {
+ return $this->transform_code_value( $value, $snippet_data );
+ }
+
+ return $value;
+ }
+
+ private function transform_scope_value( $location_value, $snippet_data ): ?string {
+ if ( ! is_scalar( $location_value ) ) {
+ return null;
+ }
+
+ $code_type = $snippet_data->snippet_type;
+
+ switch ( $code_type ) {
+ case 'html':
+ $transformations = self::HTML_SCOPE_TRANSFORMATIONS;
+ break;
+ default:
+ return null;
+ }
+
+ return $transformations[ $location_value ] ?? null;
+ }
+
+ private function transform_code_value( $code_value, $snippet_data ): ?string {
+ $code = html_entity_decode( $code_value );
+ $code_type = $snippet_data->snippet_type ?? '';
+
+ $code = $this->strip_wrapper_tags( $code, $code_type );
+ $code = $this->apply_minification( $code, $code_type );
+
+ return trim( $code );
+ }
+
+ private function strip_wrapper_tags( string $code, string $code_type ): string {
+ switch ( $code_type ) {
+ case 'css':
+ return preg_replace( '/<\s*style[^>]*>|<\s*\/\s*style\s*>/i', '', $code );
+ case 'js':
+ return preg_replace( '/<\s*script[^>]*>|<\s*\/\s*script\s*>/i', '', $code );
+ default:
+ return $code;
+ }
+ }
+
+ private function apply_minification( string $code, string $code_type ): string {
+ if ( ! in_array( $code_type, [ 'css', 'js' ], true ) ) {
+ return $code;
+ }
+
+ $setting = Settings\get_setting( 'general', 'minify_output' );
+ if ( ! is_array( $setting ) || ! in_array( $code_type, $setting, true ) ) {
+ return $code;
+ }
+
+ $minifier = 'css' === $code_type ? new Minify\CSS( $code ) : new Minify\JS( $code );
+ return $minifier->minify();
+ }
+}
diff --git a/src/php/migration/importers/plugins/importer-base.php b/src/php/migration/importers/plugins/importer-base.php
new file mode 100644
index 00000000..c84733b1
--- /dev/null
+++ b/src/php/migration/importers/plugins/importer-base.php
@@ -0,0 +1,126 @@
+create_snippet( $snippet_item, $multisite );
+
+ if ( $snippet ) {
+ if ( $auto_add_tags && ! empty( $tag_value ) ) {
+ if ( ! empty( $snippet->tags ) ) {
+ $snippet->add_tag( $tag_value );
+ } else {
+ $snippet->tags = [ $tag_value ];
+ }
+ }
+
+ $snippets[] = $snippet;
+ }
+ }
+
+ return $snippets;
+ }
+
+ public function import( $request ) {
+ $ids_to_import = $request->get_param( 'ids' ) ?? [];
+ $multisite = $request->get_param( 'network' ) ?? false;
+ $auto_add_tags = $request->get_param( 'auto_add_tags' ) ?? false;
+ $tag_value = $request->get_param( 'tag_value' ) ?? '';
+
+ $data = $this->get_data( $ids_to_import );
+
+ $snippets = $this->transform( $data, $multisite, $auto_add_tags, $tag_value );
+
+ $imported = $this->save_snippets( $snippets );
+
+ return [
+ 'imported' => $imported,
+ ];
+ }
+
+ public function get_items( $request ) {
+ return $this->get_data();
+ }
+
+ protected function save_snippets( array $snippets ): array {
+ $imported = [];
+
+ foreach ( $snippets as $snippet ) {
+ $saved_snippet = save_snippet( $snippet );
+
+ $snippet_id = $saved_snippet->id;
+
+ if ( $snippet_id ) {
+ $imported[] = $snippet_id;
+ }
+ }
+
+ return $imported;
+ }
+
+ public function register_rest_routes() {
+ $namespace = REST_API_NAMESPACE . self::VERSION;
+
+ register_rest_route( $namespace, $this->get_name(), [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'get_items' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ ] );
+
+ register_rest_route( $namespace, $this->get_name() . '/import', [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'import' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ 'args' => [
+ 'ids' => [
+ 'type' => 'array',
+ 'required' => false,
+ ],
+ 'network' => [
+ 'type' => 'boolean',
+ 'required' => false,
+ ],
+ 'auto_add_tags' => [
+ 'type' => 'boolean',
+ 'required' => false,
+ ],
+ 'tag_value' => [
+ 'type' => 'string',
+ 'required' => false,
+ ],
+ ],
+ ] );
+ }
+}
diff --git a/src/php/migration/importers/plugins/insert-headers-and-footers.php b/src/php/migration/importers/plugins/insert-headers-and-footers.php
new file mode 100644
index 00000000..98f5fdf3
--- /dev/null
+++ b/src/php/migration/importers/plugins/insert-headers-and-footers.php
@@ -0,0 +1,138 @@
+ 'name',
+ 'note' => 'desc',
+ 'code' => 'code',
+ 'tags' => 'tags',
+ 'location' => 'scope',
+ 'priority' => 'priority',
+ 'modified' => 'modified',
+ ];
+
+ private const PHP_SCOPE_TRANSFORMATIONS = [
+ 'everywhere' => 'global',
+ 'admin_only' => 'admin',
+ 'frontend_only' => 'front-end',
+ ];
+
+ private const HTML_SCOPE_TRANSFORMATIONS = [
+ '' => 'content',
+ 'site_wide_header' => 'head-content',
+ 'site_wide_footer' => 'footer-content',
+ ];
+
+ public function get_name() {
+ return 'insert-headers-and-footers';
+ }
+
+ public function get_title() {
+ return esc_html__( 'WPCode (Insert Headers and Footers)', 'code-snippets' );
+ }
+
+ public static function is_active(): bool {
+ return is_plugin_active( 'insert-headers-and-footers/ihaf.php' );
+ }
+
+ public function get_data( array $ids_to_import = [] ) {
+ $query_args = [
+ 'post_type' => 'wpcode',
+ 'post_status' => [
+ 'publish',
+ 'draft',
+ ],
+ 'nopaging' => true,
+ ];
+
+ if ( ! empty( $ids_to_import ) ) {
+ $query_args['include'] = $ids_to_import;
+ }
+
+ $data = [];
+ $snippets = get_posts( $query_args );
+
+ foreach ( $snippets as $snippet_item ) {
+ $snippet = new \WPCode_Snippet( $snippet_item );
+ $snippet_data = $snippet->get_data_for_caching();
+ $snippet_data['tags'] = $snippet->get_tags();
+ $snippet_data['note'] = $snippet->get_note();
+ $snippet_data['cloud_id'] = null;
+ $snippet_data['custom_shortcode'] = $snippet->get_custom_shortcode();
+ $snippet_data['table_data'] = [
+ 'id' => $snippet_item->ID,
+ 'title' => $snippet_item->post_title,
+ ];
+
+ $data[] = apply_filters( 'wpcode_export_snippet_data', $snippet_data, $snippet );
+ }
+
+ $data = array_reverse( $data );
+
+ return $data;
+ }
+
+ public function create_snippet( $snippet_data, bool $multisite ): ?Snippet {
+ $code_type = $snippet_data['code_type'] ?? '';
+ $is_supported_code_type = in_array( $code_type, [ 'php', 'css', 'html', 'js' ], true );
+ if ( ! $is_supported_code_type ) {
+ return null;
+ }
+
+ $snippet = new Snippet();
+ $snippet->network = $multisite;
+
+ foreach ( self::FIELD_MAPPINGS as $source_field => $target_field ) {
+ if ( ! isset( $snippet_data[ $source_field ] ) ) {
+ continue;
+ }
+
+ $value = $this->transform_field_value(
+ $target_field,
+ $snippet_data[ $source_field ],
+ $snippet_data
+ );
+
+ $scope_not_supported = 'scope' === $target_field && null === $value;
+ if ( $scope_not_supported ) {
+ return null;
+ }
+
+ $snippet->set_field( $target_field, $value );
+ }
+
+ return $snippet;
+ }
+
+ private function transform_field_value( string $target_field, $value, array $snippet_data ) {
+ if ( 'scope' === $target_field ) {
+ return $this->transform_scope_value( $value, $snippet_data );
+ }
+
+ return $value;
+ }
+
+ private function transform_scope_value( $location_value, array $snippet_data ): ?string {
+ if ( ! is_scalar( $location_value ) ) {
+ return null;
+ }
+
+ $code_type = $snippet_data['code_type'];
+
+ switch ( $code_type ) {
+ case 'html':
+ $transformations = self::HTML_SCOPE_TRANSFORMATIONS;
+ break;
+ case 'php':
+ $transformations = self::PHP_SCOPE_TRANSFORMATIONS;
+ break;
+ default:
+ return null;
+ }
+
+ return $transformations[ $location_value ] ?? null;
+ }
+}
diff --git a/src/php/migration/importers/plugins/insert-php-code-snippet.php b/src/php/migration/importers/plugins/insert-php-code-snippet.php
new file mode 100644
index 00000000..7e5bb579
--- /dev/null
+++ b/src/php/migration/importers/plugins/insert-php-code-snippet.php
@@ -0,0 +1,125 @@
+ 'name',
+ 'content' => 'code',
+ 'insertionLocationType' => 'scope',
+ ];
+
+ private const SCOPE_TRANSFORMATIONS = [
+ 0 => 'single-use',
+ 2 => 'admin',
+ 3 => 'front-end',
+ ];
+
+ private const SHORTCODE_SCOPE_TRANSFORMATIONS = [
+ 3 => 'content',
+ ];
+
+ public function get_name() {
+ return 'insert-php-code-snippet';
+ }
+
+ public function get_title() {
+ return esc_html__( 'Insert PHP Code Snippet', 'code-snippets' );
+ }
+
+ public static function is_active(): bool {
+ return is_plugin_active( 'insert-php-code-snippet/insert-php-code-snippet.php' );
+ }
+
+ public function get_data( array $ids_to_import = [] ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'xyz_ips_short_code';
+ $sql = "SELECT * FROM `{$table_name}`";
+
+ if ( ! empty( $ids_to_import ) ) {
+ $sql .= " WHERE id IN (" . implode( ',', $ids_to_import ) . ")";
+ }
+
+ $snippets = $wpdb->get_results(
+ $sql
+ );
+
+ foreach ( $snippets as $snippet ) {
+ $snippet->table_data = [
+ 'id' => (int) $snippet->id,
+ 'title' => $snippet->title,
+ ];
+ }
+
+ return $snippets;
+ }
+
+ public function create_snippet( $snippet_data, bool $multisite ): ?Snippet {
+ $code_type = $snippet_data->snippet_type ?? '';
+
+ $snippet = new Snippet();
+ $snippet->network = $multisite;
+
+ foreach ( self::FIELD_MAPPINGS as $source_field => $target_field ) {
+ if ( ! isset( $snippet_data->$source_field ) ) {
+ continue;
+ }
+
+ $value = $this->transform_field_value(
+ $target_field,
+ $snippet_data->$source_field,
+ $snippet_data
+ );
+
+ $scope_not_supported = 'scope' === $target_field && null === $value;
+ if ( $scope_not_supported ) {
+ return null;
+ }
+
+ $snippet->set_field( $target_field, $value );
+ }
+
+ return $snippet;
+ }
+
+ private function transform_field_value( string $target_field, $value, $snippet_data ) {
+ if ( 'scope' === $target_field ) {
+ return $this->transform_scope_value( $value, $snippet_data );
+ }
+
+ if ( 'code' === $target_field ) {
+ return $this->transform_code_value( $value, $snippet_data );
+ }
+
+ return $value;
+ }
+
+ private function transform_scope_value( $location_value, $snippet_data ): ?string {
+ if ( ! is_scalar( $location_value ) ) {
+ return null;
+ }
+
+ $transformations = self::SCOPE_TRANSFORMATIONS;
+
+ if ( '2' === $snippet_data->insertionMethod ) {
+ $transformations = self::SHORTCODE_SCOPE_TRANSFORMATIONS;
+ }
+
+ return $transformations[ $location_value ] ?? null;
+ }
+
+ private function transform_code_value( $code_value, $snippet_data ): ?string {
+ $code = html_entity_decode( $code_value );
+
+ if ( '2' !== $snippet_data->insertionMethod ) {
+ $code = $this->strip_wrapper_tags( $code );
+ }
+
+ return trim( $code );
+ }
+
+ private function strip_wrapper_tags( string $code ): string {
+ return preg_replace( '/^\s*<\?\s*(php)?\s*|\?\>\s*$/i', '', $code );
+ }
+}
diff --git a/src/php/migration/importers/plugins/manager.php b/src/php/migration/importers/plugins/manager.php
new file mode 100644
index 00000000..013541d4
--- /dev/null
+++ b/src/php/migration/importers/plugins/manager.php
@@ -0,0 +1,60 @@
+init_plugin_importers();
+ add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
+ }
+
+ private function init_plugin_importers() {
+ $this->plugin_importers = [
+ 'insert-headers-and-footers' => new Insert_Headers_And_Footers_Importer(),
+ 'header-footer-code-manager' => new Header_Footer_Code_Manager_Importer(),
+ 'insert-php-code-snippet' => new Insert_PHP_Code_Snippet_Importer(),
+ ];
+ }
+
+ public function get_importer( string $source ) {
+ return $this->plugin_importers[ $source ] ?? null;
+ }
+
+ public function get_importers() {
+ if ( empty( $this->plugin_importers ) ) {
+ $this->init_plugin_importers();
+ }
+
+ $plugins_list = [];
+
+ foreach ( $this->plugin_importers as $importer ) {
+ $plugins_list[] = [
+ 'name' => $importer->get_name(),
+ 'title' => $importer->get_title(),
+ 'is_active' => $importer::is_active(),
+ ];
+ }
+
+ return $plugins_list;
+ }
+
+ public function register_rest_routes() {
+ $namespace = REST_API_NAMESPACE . self::VERSION;
+
+ register_rest_route( $namespace, 'importers', [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'get_importers' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ ] );
+ }
+}
diff --git a/src/php/views/import.php b/src/php/views/import.php
index 902d4409..11f372d8 100644
--- a/src/php/views/import.php
+++ b/src/php/views/import.php
@@ -19,7 +19,6 @@
}
$max_size_bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() );
-
?>
@@ -34,88 +33,5 @@
?>
- print_messages(); ?>
-
-
-
-
-
-
- All Snippets page to activate the imported snippets.', 'code-snippets' );
- $url = esc_url( code_snippets()->get_menu_url( 'manage' ) );
-
- echo wp_kses(
- sprintf( $text, $url ),
- array(
- 'a' => array(
- 'href' => array(),
- 'target' => array(),
- ),
- )
- );
- ?>
-
-
-
-
+
From 41c9ba32f40d363b774a1cd9ecfe6f2c72ecdfe7 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:05:38 +0200
Subject: [PATCH 02/31] Update
src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/ImportResultDisplay.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
index 92c762a9..8aa499cf 100644
--- a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
+++ b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
@@ -58,7 +58,7 @@ export const ImportResultDisplay: React.FC = ({ result
{__('Warnings:', 'code-snippets')}
-
+
{result.warnings.map((warning, index) => (
{warning}
From 5f67617db5e4d9b67e4617871eb8bd83f6aa672c Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:05:46 +0200
Subject: [PATCH 03/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index 6369b89e..b5afbe64 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -146,7 +146,7 @@ export const FileUploadForm: React.FC = () => {
-
+
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
From 90f4f553f16c6a00b0dcdc1c10f371c36c4d2425 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:05:59 +0200
Subject: [PATCH 04/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index b5afbe64..8c53e5ac 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -173,7 +173,7 @@ export const FileUploadForm: React.FC = () => {
/>
-
+
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
From 840d553a4fbbf52ea90d66d64ad6be1c3c78c411 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:06:22 +0200
Subject: [PATCH 05/31] Update
src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/SimpleSnippetTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
index f6c7f8ec..99d14b44 100644
--- a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
@@ -31,7 +31,7 @@ export const SimpleSnippetTable: React.FC = ({
{__('We found the following snippets.', 'code-snippets')}
-
+
{isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
From 9b3c02ef5467d2b7d0827ab76098704830e754cd Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:06:44 +0200
Subject: [PATCH 06/31] Update
src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/SimpleSnippetTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
index 99d14b44..b6ed4b07 100644
--- a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
@@ -81,7 +81,7 @@ export const SimpleSnippetTable: React.FC = ({
-
+
{isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
From 30d897b1a1edfaf79ac4e3eabf5b37270553ecf1 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:07:14 +0200
Subject: [PATCH 07/31] Update
src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/SnippetSelectionTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx b/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
index 8a59d197..f31d468f 100644
--- a/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
+++ b/src/js/components/Import/FromFileUpload/components/SnippetSelectionTable.tsx
@@ -61,7 +61,7 @@ export const SnippetSelectionTable: React.FC = ({
{snippet.table_data.title}
{snippet.source_file && (
-
+
from {snippet.source_file}
)}
From 6a057c4d2dcba31085c702fd5ebbe811711de921 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:07:28 +0200
Subject: [PATCH 08/31] Update
src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
Co-authored-by: Rami Yushuvaev
---
.../FromFileUpload/components/DuplicateActionSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
index b4757da0..242e5301 100644
--- a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
@@ -29,7 +29,7 @@ export const DuplicateActionSelector: React.FC = (
value="ignore"
checked={value === 'ignore'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
- style={{ marginTop: '2px' }}
+ style={{ marginBlockStart: '2px' }}
/>
{__('Ignore any duplicate snippets: import all snippets from the file regardless and leave all existing snippets unchanged.', 'code-snippets')}
From d7b01a275b283bf50665ebcae8b602908b8744ee Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:07:43 +0200
Subject: [PATCH 09/31] Update
src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
Co-authored-by: Rami Yushuvaev
---
.../FromFileUpload/components/DuplicateActionSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
index 242e5301..34e19ce9 100644
--- a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
@@ -43,7 +43,7 @@ export const DuplicateActionSelector: React.FC = (
value="replace"
checked={value === 'replace'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
- style={{ marginTop: '2px' }}
+ style={{ marginBlockStart: '2px' }}
/>
{__('Replace any existing snippets with a newly imported snippet of the same name.', 'code-snippets')}
From c7e0e7c52a1ddc6eb55c64876818e2e6f12aeb07 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:08:23 +0200
Subject: [PATCH 10/31] Update
src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
Co-authored-by: Rami Yushuvaev
---
.../FromFileUpload/components/DuplicateActionSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
index 34e19ce9..5d1ead1b 100644
--- a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
@@ -57,7 +57,7 @@ export const DuplicateActionSelector: React.FC = (
value="skip"
checked={value === 'skip'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
- style={{ marginTop: '2px' }}
+ style={{ marginBlockStart: '2px' }}
/>
{__('Do not import any duplicate snippets; leave all existing snippets unchanged.', 'code-snippets')}
From 5fe03284cdcbb102b8c99f42067b813e5d7e959a Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:08:39 +0200
Subject: [PATCH 11/31] Update
src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/ImportResultDisplay.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
index 8aa499cf..3bc8a995 100644
--- a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
+++ b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
@@ -26,7 +26,7 @@ export const ImportResultDisplay: React.FC = ({ result
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
- marginTop: '2px'
+ marginBlockStart: '2px'
}}>
{result.success ? '✓' : '✕'}
From 56abcb59d3f886e3afb3f0b76c0fdb4ae57801f3 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:08:53 +0200
Subject: [PATCH 12/31] Update
src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/ImportResultDisplay.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
index 3bc8a995..38bd186e 100644
--- a/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
+++ b/src/js/components/Import/FromFileUpload/components/ImportResultDisplay.tsx
@@ -54,7 +54,7 @@ export const ImportResultDisplay: React.FC = ({ result
)}
{result.warnings && result.warnings.length > 0 && (
-
+
{__('Warnings:', 'code-snippets')}
From 900a0d8cc57069f275d03f27017224f4f03135ed Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:09:19 +0200
Subject: [PATCH 13/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index 8c53e5ac..0521681e 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -172,7 +172,7 @@ export const FileUploadForm: React.FC = () => {
onSelectAll={snippetSelection.handleSelectAll}
/>
-
+
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
From 5bff754a78460aa4c88c26ba4daa3ad709527813 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:09:48 +0200
Subject: [PATCH 14/31] Update
src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/ImporterSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx b/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
index bed4d287..010358b9 100644
--- a/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/ImporterSelector.tsx
@@ -26,7 +26,7 @@ export const ImporterSelector: React.FC = ({
value={selectedImporter}
onChange={(event) => onImporterChange(event.target.value)}
className="regular-text"
- style={{ display: 'block', marginTop: '5px', width: '100%', maxWidth: '300px' }}
+ style={{ display: 'block', marginBlockStart: '5px', width: '100%', maxWidth: '300px' }}
disabled={isLoading}
>
{__('-- Select an importer --', 'code-snippets')}
From bd6633ad48af5630ff1337b6c29a6fc520cc5d8b Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:10:00 +0200
Subject: [PATCH 15/31] Update
src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/ImportOptions.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
index ac084959..34285fec 100644
--- a/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
@@ -23,7 +23,7 @@ export const ImportOptions: React.FC = ({
type="checkbox"
checked={autoAddTags}
onChange={(e) => onAutoAddTagsChange(e.target.checked)}
- style={{ marginTop: '2px' }}
+ style={{ marginBlockStart: '2px' }}
/>
From c6c200c985e46ba7018b925373d58f0f992695eb Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:10:13 +0200
Subject: [PATCH 16/31] Update
src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/StatusDisplay.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
index 09e0b39a..0feffa9e 100644
--- a/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
@@ -28,7 +28,7 @@ export const StatusDisplay: React.FC = ({
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
- marginTop: '2px'
+ marginBlockStart: '2px'
}}>
{isError ? '✕' : '✓'}
From d46d38eb2b1611336b22ca31b9e758d1f59d78ce Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:10:27 +0200
Subject: [PATCH 17/31] Update
src/js/components/Import/shared/components/ImportSection.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/shared/components/ImportSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/shared/components/ImportSection.tsx b/src/js/components/Import/shared/components/ImportSection.tsx
index 262798c9..255fd2a3 100644
--- a/src/js/components/Import/shared/components/ImportSection.tsx
+++ b/src/js/components/Import/shared/components/ImportSection.tsx
@@ -17,7 +17,7 @@ export const ImportSection: React.FC = ({
}) => {
const sectionStyle: React.CSSProperties = {
display: active ? 'block' : 'none',
- paddingTop: 0,
+ paddingBlockStart: 0,
...style
}
From 9c10716d27ef1f1ac618ca6249c514843d7f665d Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:11:01 +0200
Subject: [PATCH 18/31] Update
src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/ImportOptions.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
index 34285fec..66fd1b02 100644
--- a/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/ImportOptions.tsx
@@ -34,7 +34,7 @@ export const ImportOptions: React.FC = ({
{autoAddTags && (
-
+
Date: Fri, 5 Dec 2025 11:11:32 +0200
Subject: [PATCH 19/31] Update
src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/SimpleSnippetTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
index b6ed4b07..41b405bb 100644
--- a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
@@ -80,7 +80,7 @@ export const SimpleSnippetTable: React.FC = ({
-
+
{isAllSelected
? __('Deselect All', 'code-snippets')
From 895744032a1775f30a2375b1908b014f28df663e Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:11:44 +0200
Subject: [PATCH 20/31] Update
src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/DragDropUploadArea.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
index 1c8b6029..51ce0c91 100644
--- a/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
@@ -38,7 +38,7 @@ export const DragDropUploadArea: React.FC = ({
textAlign: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: dragOver ? '#f0f6fc' : disabled ? '#f6f7f7' : '#fafafa',
- marginBottom: '20px',
+ marginBlockEnd: '20px',
transition: 'all 0.3s ease',
opacity: disabled ? 0.6 : 1
}}
From 048a9783ec58cdf09f55240053226cf3d1758edf Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:12:06 +0200
Subject: [PATCH 21/31] Update
src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/DragDropUploadArea.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
index 51ce0c91..15ead294 100644
--- a/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DragDropUploadArea.tsx
@@ -43,7 +43,7 @@ export const DragDropUploadArea: React.FC = ({
opacity: disabled ? 0.6 : 1
}}
>
- 📁
+ 📁
{__('Drag and drop files here, or click to browse', 'code-snippets')}
From 943b28ff4c2dad1f4a7825731d9fe5f12cda8798 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:12:16 +0200
Subject: [PATCH 22/31] Update
src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
Co-authored-by: Rami Yushuvaev
---
.../FromFileUpload/components/DuplicateActionSelector.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
index 5d1ead1b..cd8a28b8 100644
--- a/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
+++ b/src/js/components/Import/FromFileUpload/components/DuplicateActionSelector.tsx
@@ -16,7 +16,7 @@ export const DuplicateActionSelector: React.FC = (
return (
{__('Duplicate Snippets', 'code-snippets')}
-
+
{__('What should happen if an existing snippet is found with an identical name to an imported snippet?', 'code-snippets')}
From 1eba64fd9d3053b44fee3bf2a2c83617a84492d4 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:12:24 +0200
Subject: [PATCH 23/31] Update
src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromFileUpload/components/SelectedFilesList.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx b/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
index 3bf11aa3..0cc70c49 100644
--- a/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
+++ b/src/js/components/Import/FromFileUpload/components/SelectedFilesList.tsx
@@ -12,7 +12,7 @@ export const SelectedFilesList: React.FC = ({
onRemoveFile
}) => {
return (
-
+
{__('Selected Files:', 'code-snippets')} ({files.length})
From 9378b56722ede93424c7dfb02c760d6f62fab5ae Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Fri, 5 Dec 2025 11:12:33 +0200
Subject: [PATCH 24/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index 0521681e..b13a4cff 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -95,7 +95,7 @@ export const FileUploadForm: React.FC = () => {
{__('Choose Files', 'code-snippets')}
-
+
{__('Choose one or more Code Snippets (.xml or .json) files to parse and preview.', 'code-snippets')}
From 094039ca49bb72c9618f4c461891ad8262c5ddd0 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:55:48 +0200
Subject: [PATCH 25/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index b13a4cff..4ceefe1e 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -133,7 +133,7 @@ export const FileUploadForm: React.FC = () => {
{currentStep === 'select' && importWorkflow.availableSnippets.length > 0 && !importWorkflow.uploadResult?.success && (
-
+
{__('← Upload Different Files', 'code-snippets')}
From cb3b1c566acb13a6c47698b04a0a80bf38d6fb36 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:55:55 +0200
Subject: [PATCH 26/31] Update
src/js/components/Import/FromFileUpload/FileUploadForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromFileUpload/FileUploadForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
index 4ceefe1e..f2c8b7b3 100644
--- a/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
+++ b/src/js/components/Import/FromFileUpload/FileUploadForm.tsx
@@ -138,7 +138,7 @@ export const FileUploadForm: React.FC = () => {
{__('← Upload Different Files', 'code-snippets')}
-
+
{__('Available Snippets', 'code-snippets')} ({importWorkflow.availableSnippets.length})
From 9a82f558f9452bc375df3bbe62a14bf27fc05f37 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:56:01 +0200
Subject: [PATCH 27/31] Update
src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/SimpleSnippetTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
index 41b405bb..30e3a823 100644
--- a/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/SimpleSnippetTable.tsx
@@ -25,7 +25,7 @@ export const SimpleSnippetTable: React.FC = ({
return (
-
+
{__('Available Snippets', 'code-snippets')} ({snippets.length})
{__('We found the following snippets.', 'code-snippets')}
From 1b5d37faaa347ccff0f31fb312338c18b07f3e95 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:56:11 +0200
Subject: [PATCH 28/31] Update
src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
Co-authored-by: Rami Yushuvaev
---
.../Import/FromOtherPlugins/components/StatusDisplay.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
index 0feffa9e..e5d0e919 100644
--- a/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
+++ b/src/js/components/Import/FromOtherPlugins/components/StatusDisplay.tsx
@@ -18,7 +18,7 @@ export const StatusDisplay: React.FC = ({
const isError = type === 'error'
return (
-
+
Date: Thu, 18 Dec 2025 14:56:21 +0200
Subject: [PATCH 29/31] Update
src/js/components/Import/FromOtherPlugins/ImportForm.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/FromOtherPlugins/ImportForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/FromOtherPlugins/ImportForm.tsx b/src/js/components/Import/FromOtherPlugins/ImportForm.tsx
index 5c61cc50..383e2aed 100644
--- a/src/js/components/Import/FromOtherPlugins/ImportForm.tsx
+++ b/src/js/components/Import/FromOtherPlugins/ImportForm.tsx
@@ -106,7 +106,7 @@ export const ImportForm: React.FC = () => {
snippetImport.importSuccess.length === 0 && (
-
📭
+
📭
{__('No snippets found', 'code-snippets')}
From 6496b3dc1163b9c18eb2d253eab3418b48542826 Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:56:38 +0200
Subject: [PATCH 30/31] Update
src/js/components/Import/shared/components/ImportCard.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/shared/components/ImportCard.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/shared/components/ImportCard.tsx b/src/js/components/Import/shared/components/ImportCard.tsx
index 64d8182f..f2420de1 100644
--- a/src/js/components/Import/shared/components/ImportCard.tsx
+++ b/src/js/components/Import/shared/components/ImportCard.tsx
@@ -20,7 +20,7 @@ export const ImportCard = React.forwardRef(({
padding: '25px',
borderRadius: '5px',
border: '1px solid #e0e0e0',
- marginBottom: '10px',
+ marginBlockEnd: '10px',
width: '100%',
...style
}
From f97ce32829f88187a248a91424a7cb25f0b1d83f Mon Sep 17 00:00:00 2001
From: Louis Wolmarans
Date: Thu, 18 Dec 2025 14:56:46 +0200
Subject: [PATCH 31/31] Update src/js/components/Import/ImportApp.tsx
Co-authored-by: Rami Yushuvaev
---
src/js/components/Import/ImportApp.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/js/components/Import/ImportApp.tsx b/src/js/components/Import/ImportApp.tsx
index 2d036bdd..a000f562 100644
--- a/src/js/components/Import/ImportApp.tsx
+++ b/src/js/components/Import/ImportApp.tsx
@@ -27,7 +27,7 @@ export const ImportApp: React.FC = () => {
return (