PluginProbe
TablePress – Tables in WordPress made easy / 3.0
TablePress – Tables in WordPress made easy v3.0
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / admin / js / import / screen.jsx

screen.jsx in TablePress – Tables in WordPress made easy 3.0, at admin/js/import/screen.jsx

329 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * JavaScript code for the "Import Screen" component.
3 *
4 * @package TablePress
5 * @subpackage Import Screen
6 * @author Tobias Bäthge
7 * @since 2.2.0
8 */
9
10 /* globals tp */
11
12 /**
13 * WordPress dependencies.
14 */
15 import { useEffect, useRef, useState } from 'react';
16 import {
17 Button,
18 __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
19 Icon,
20 RadioControl,
21 ComboboxControl,
22 Disabled,
23 TextareaControl,
24 TextControl,
25 } from '@wordpress/components';
26 import { info } from '@wordpress/icons';
27 import { __, _n, _x, sprintf } from '@wordpress/i18n';
28
29 // Details for the available import sources.
30 const importSources = {
31 'file-upload': {
32 label: __( 'File Upload', 'tablepress' ),
33 instruction: __( 'Select files', 'tablepress' ),
34 },
35 url: {
36 label: __( 'URL', 'tablepress' ),
37 instruction: __( 'File URL', 'tablepress' ),
38 },
39 server: {
40 label: __( 'File on server', 'tablepress' ),
41 instruction: __( 'Server Path to file', 'tablepress' ),
42 },
43 'form-field': {
44 label: __( 'Manual Input', 'tablepress' ),
45 instruction: __( 'Import data', 'tablepress' ),
46 },
47 };
48 if ( ! tp.import.showImportSourceUrl ) {
49 delete importSources.url;
50 }
51 if ( ! tp.import.showImportSourceServer ) {
52 delete importSources.server;
53 }
54
55 const importSourcesRadioOptions = Object.entries( importSources ).map( ( [ importSource, importSourceData ] ) => ( { value: importSource, label: importSourceData.label } ) );
56
57 // Number of tables.
58 const tablesCount = Object.keys( tp.import.tables ).length;
59
60 const tablesSelectOptions = Object.entries( tp.import.tables ).map( ( [ tableId, tableName ] ) => {
61 if ( '' === tableName.trim() ) {
62 tableName = __( '(no name)', 'tablepress' );
63 }
64 const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName );
65 return { value: tableId, label: optionText };
66 } );
67
68 // Custom component to conditionally disable its children, used for the ComboboxControl.
69 const ConditionalDisabled = ( { condition, children } ) => (
70 condition ? ( <Disabled>{ children }</Disabled> ) : children
71 );
72
73 /**
74 * Returns the "Import Screen" component's JSX markup.
75 *
76 * @return {Object} Import Screen component.
77 */
78 const Screen = () => {
79 const [ screenData, setScreenData ] = useState( {
80 importSource: tp.import.importSource,
81 importType: tp.import.importType,
82 importFileUpload: [],
83 importUrl: tp.import.importUrl,
84 importServer: tp.import.importServer,
85 importFormField: tp.import.importFormField,
86 importExistingTable: tp.import.importExistingTable,
87 validationHighlighting: false,
88 } );
89
90 /**
91 * Handles screen data state changes.
92 *
93 * @param {Object} updatedScreenData Data in the screen data state that should be updated.
94 */
95 const updateScreenData = ( updatedScreenData ) => {
96 setScreenData( ( currentScreenData ) => ( {
97 ...currentScreenData,
98 validationHighlighting: false, // Reset with every UI state change.
99 ...updatedScreenData,
100 } ) );
101 };
102
103 // References to DOM elements.
104 const importServerInput = useRef( null );
105 const fileUploadDropzone = useRef( null );
106
107 // Update the validation highlighting (using APIs and DOM elements outside of the React components) when the state changes.
108 useEffect( () => {
109 document.getElementById( 'tablepress_import-import-form' ).classList.toggle( 'no-validation-highlighting', ! screenData.validationHighlighting );
110 if ( ! screenData.validationHighlighting ) {
111 importServerInput.current?.setCustomValidity( '' );
112 // We need to use this dynamically generated ID by the ComboboxControl component. It does not (yet?) support a static ID or a ref.
113 document.getElementById( 'components-form-token-input-combobox-control-1' )?.setCustomValidity( '' );
114 }
115 }, [ screenData.validationHighlighting ] );
116
117 // Determine calculated state variables to avoid repeating calculations.
118 const fileUploadMultipleFilesChosen = (
119 'file-upload' === screenData.importSource
120 && (
121 1 < screenData.importFileUpload.length
122 || (
123 1 === screenData.importFileUpload.length
124 && screenData.importFileUpload[0].name.endsWith( '.zip' )
125 )
126 )
127 );
128 const appendReplaceDropdownDisabled = (
129 0 === tablesCount
130 || 'add' === screenData.importType
131 || fileUploadMultipleFilesChosen
132 );
133
134 return (
135 <table className="tablepress-postbox-table fixed">
136 <tbody>
137 <tr>
138 <th className="column-1" scope="row">
139 { __( 'Import Source', 'tablepress' ) }:
140 </th>
141 <td className="column-2">
142 <RadioControl
143 name="import[source]"
144 label={ __( 'Import Source', 'tablepress' ) }
145 hideLabelFromVision={ true }
146 selected={ screenData.importSource }
147 onChange={ ( importSource ) => updateScreenData( { importSource } ) }
148 options={ importSourcesRadioOptions }
149 />
150 </td>
151 </tr>
152 <tr className="top-border bottom-border">
153 <th className="column-1 top-align" scope="row">
154 <label htmlFor={ `tables-import-${ screenData.importSource }` }>
155 { importSources[ screenData.importSource ].instruction }:
156 </label>
157 </th>
158 <td className="column-2">
159 {
160 /*
161 * Always add the "File Upload" UI to the DOM, but hide it using `style="display: none;"` below.
162 * This ensures that the <input type="file"> field works, as that is "uncontrolled" in React, and setting its value (files) is not possible.
163 */
164 }
165 <div
166 className="file-upload-area"
167 style={ {
168 display: ( 'file-upload' === screenData.importSource ) ? 'block' : 'none',
169 } }
170 >
171 <input
172 name="import_file_upload[]"
173 id="tables-import-file-upload"
174 type="file"
175 multiple
176 required={ 'file-upload' === screenData.importSource }
177 onChange={ ( event ) => ( event.target.files && updateScreenData( { importFileUpload: event.target.files } ) ) }
178 onDragEnter={ () => fileUploadDropzone.current.classList.add( 'dragover' ) }
179 onDragLeave={ () => fileUploadDropzone.current.classList.remove( 'dragover' ) }
180 />
181 <div
182 ref={ fileUploadDropzone }
183 id="tables-import-file-upload-dropzone"
184 className="dropzone"
185 >
186 <span>
187 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
188 { 0 < screenData.importFileUpload.length && sprintf( _n( 'You have selected %1$d file:', 'You have selected %1$d files:', screenData.importFileUpload.length, 'tablepress' ), screenData.importFileUpload.length ) }
189 </span>
190 {
191 [ ...screenData.importFileUpload ].map( ( file ) =>
192 <span key={ file.name }>{ file.name }</span>
193 )
194 }
195 </div>
196 </div>
197 { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
198 <TextControl
199 __nextHasNoMarginBottom
200 type="url"
201 name="import[url]"
202 id="tables-import-url"
203 className="code"
204 required={ true }
205 value={ screenData.importUrl }
206 onChange={ ( importUrl ) => updateScreenData( { importUrl } ) }
207 />
208 }
209 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
210 <TextControl
211 __nextHasNoMarginBottom
212 ref={ importServerInput }
213 name="import[server]"
214 id="tables-import-server"
215 className="code"
216 required={ true }
217 value={ screenData.importServer }
218 onChange={ ( importServer ) => updateScreenData( { importServer } ) }
219 />
220 }
221 { 'form-field' === screenData.importSource &&
222 <TextareaControl
223 __nextHasNoMarginBottom
224 name="import[form-field]"
225 id="tables-import-form-field"
226 rows="15"
227 cols="40"
228 className="code"
229 required={ true }
230 value={ screenData.importFormField }
231 onChange={ ( importFormField ) => updateScreenData( { importFormField } ) }
232 />
233 }
234 { 'form-field' !== screenData.importSource &&
235 <HStack
236 alignment="left"
237 >
238 <Icon icon={ info } />
239 <span>
240 { __( 'You can also import multiple tables by placing them in a ZIP file.', 'tablepress' ) }
241 </span>
242 </HStack>
243 }
244 </td>
245 </tr>
246 <tr className="top-border">
247 <th className="column-1" scope="row">
248 { __( 'Add, Replace, or Append?', 'tablepress' ) }:
249 </th>
250 <td className="column-2">
251 <RadioControl
252 name="import[type]"
253 label={ __( 'Import Type', 'tablepress' ) }
254 hideLabelFromVision={ true }
255 selected={ 0 === tablesCount ? 'add' : screenData.importType } // Always select "Add" if there are no tables.
256 onChange={ ( importType ) => updateScreenData( { importType } ) }
257 options={ [
258 { value: 'add', label: __( 'Add as new table', 'tablepress' ) },
259 { value: 'replace', label: __( 'Replace existing table', 'tablepress' ), disabled: 0 === tablesCount },
260 { value: 'append', label: __( 'Append rows to existing table', 'tablepress' ), disabled: 0 === tablesCount },
261 ] }
262 />
263 </td>
264 </tr>
265 <tr className="top-border bottom-border">
266 <th className="column-1 top-align" scope="row">
267 <label htmlFor="tables-import-existing-table">{ __( 'Table to replace or append to', 'tablepress' ) }:</label>
268 </th>
269 <td className="column-2">
270 <ConditionalDisabled
271 condition={ appendReplaceDropdownDisabled }
272 >
273 <ComboboxControl
274 __nextHasNoMarginBottom
275 id="tables-import-existing-table"
276 label={ __( 'Table to replace or append to', 'tablepress' ) }
277 hideLabelFromVision={ true }
278 placeholder={ __( '— Select or type —', 'tablepress' ) }
279 value={ screenData.importExistingTable }
280 options={ tablesSelectOptions }
281 onChange={ ( importExistingTable ) => updateScreenData( { importExistingTable } ) }
282 />
283 </ConditionalDisabled>
284 </td>
285 </tr>
286 <tr className="top-border">
287 <td className="column-1"></td>
288 <td className="column-2">
289 <input
290 // Send the legacy import flag to the server, so that it can handle the import accordingly.
291 type="hidden"
292 name="import[legacy_import]"
293 value={ tp.import.legacyImport }
294 />
295 <input
296 // Send the Table to be replaced/appended to the server, if a table was selected. The ComboboxControl is not an actual form element with a name.
297 type="hidden"
298 name="import[existing_table]"
299 value={ screenData.importExistingTable ?? '' }
300 />
301 <Button
302 variant="primary"
303 type="submit"
304 text={ _x( 'Import', 'button', 'tablepress' ) }
305 onClick={ () => {
306 // Show validation :invalid CSS pseudo-selector highlighting.
307 updateScreenData( { validationHighlighting: true } );
308
309 // When importing from the server, the value must have been changed from the default (normally ABSPATH).
310 if ( 'server' === screenData.importSource && tp.import.importServer === screenData.importServer ) {
311 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
312 }
313
314 // If the table selection dropdown for replace or append is enabled, a table must be selected.
315 if ( ! appendReplaceDropdownDisabled && ! screenData.importExistingTable ) {
316 // We need to use this dynamically generated ID by the ComboboxControl component. It does not (yet?) support a static ID or a ref.
317 document.getElementById( 'components-form-token-input-combobox-control-1' )?.setCustomValidity( __( 'You must select a table.', 'tablepress' ) );
318 }
319 } }
320 />
321 </td>
322 </tr>
323 </tbody>
324 </table>
325 );
326 };
327
328 export default Screen;
329