PluginProbe
TablePress – Tables in WordPress made easy / 2.4.2
TablePress – Tables in WordPress made easy v2.4.2
3.4 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 All 45 releases
tablepress / admin / js / import / screen.jsx

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

364 lines 12.3 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 Icon,
18 } from '@wordpress/components';
19 import { info } from '@wordpress/icons';
20 import { __, _n, _x, sprintf } from '@wordpress/i18n';
21
22 // Details for the available import sources.
23 const importSources = {
24 'file-upload': {
25 label: __( 'File Upload', 'tablepress' ),
26 instruction: __( 'Select files', 'tablepress' ),
27 },
28 url: {
29 label: __( 'URL', 'tablepress' ),
30 instruction: __( 'File URL', 'tablepress' ),
31 },
32 server: {
33 label: __( 'File on server', 'tablepress' ),
34 instruction: __( 'Server Path to file', 'tablepress' ),
35 },
36 'form-field': {
37 label: __( 'Manual Input', 'tablepress' ),
38 instruction: __( 'Import data', 'tablepress' ),
39 },
40 };
41 if ( ! tp.import.showImportSourceUrl ) {
42 delete importSources.url;
43 }
44 if ( ! tp.import.showImportSourceServer ) {
45 delete importSources.server;
46 }
47
48 // Number of tables.
49 const tablesCount = Object.keys( tp.import.tables ).length;
50
51 // The <option> entries for the dropdown do not depend on the state, so they can be created once.
52 const tablesSelectOptions = Object.entries( tp.import.tables ).map( ( [ tableId, tableName ] ) => {
53 if ( '' === tableName.trim() ) {
54 tableName = __( '(no name)', 'tablepress' );
55 }
56 const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName );
57 return <option key={ tableId } value={ tableId }>{ optionText }</option>;
58 } );
59
60 /**
61 * Returns the "Import Screen" component's JSX markup.
62 *
63 * @return {Object} Import Screen component.
64 */
65 const Screen = () => {
66 const [ screenData, setScreenData ] = useState( {
67 importSource: tp.import.importSource,
68 importType: tp.import.importType,
69 importFileUpload: [],
70 importUrl: tp.import.importUrl,
71 importServer: tp.import.importServer,
72 importFormField: tp.import.importFormField,
73 importExistingTable: tp.import.importExistingTable,
74 validationHighlighting: false,
75 } );
76
77 /**
78 * Handles screen data state changes.
79 *
80 * @param {Object} updatedData Data in the screen data state that should be updated.
81 */
82 const updateScreenData = ( updatedData ) => {
83 const newScreenData = {
84 ...screenData,
85 validationHighlighting: false, // Reset with every UI state change.
86 ...updatedData,
87 };
88 setScreenData( newScreenData );
89 };
90
91 // References to DOM elements.
92 const importServerInput = useRef( null );
93 const appendReplaceDropdown = useRef( null );
94 const fileUploadDropzone = useRef( null );
95
96 // Initialize the jSuites dropdown when the component is mounted.
97 useEffect( () => {
98 jSuites.dropdown( appendReplaceDropdown.current, {
99 autocomplete: true,
100 placeholder: __( '— Select or type —', 'tablepress' ),
101 onchange: ( element, index, oldValue, newValue ) => {
102 // Directly update the state with an updater function, as the state is otherwise reset.
103 setScreenData( ( newScreenData ) => ( {
104 ...newScreenData,
105 validationHighlighting: false,
106 importExistingTable: newValue,
107 } ) );
108 },
109 } );
110 }, [] );
111
112 // Update the validation highlighting (using APIs and DOM elements outside of the React components) when the state changes.
113 useEffect( () => {
114 document.getElementById( 'tablepress_import-import-form' ).classList.toggle( 'no-validation-highlighting', ! screenData.validationHighlighting );
115 if ( ! screenData.validationHighlighting ) {
116 importServerInput.current?.setCustomValidity( '' );
117 appendReplaceDropdown.current.previousElementSibling.querySelector( '.jdropdown-header' )?.setCustomValidity( '' );
118 }
119 }, [ screenData.validationHighlighting ] );
120
121 // Determine calculated state variables to avoid repeating calculations.
122 const fileUploadMultipleFilesChosen = (
123 'file-upload' === screenData.importSource
124 && (
125 1 < screenData.importFileUpload.length
126 || (
127 1 === screenData.importFileUpload.length
128 && screenData.importFileUpload[0].name.endsWith( '.zip' )
129 )
130 )
131 );
132 const appendReplaceDropdownDisabled = (
133 0 === tablesCount
134 || 'add' === screenData.importType
135 || fileUploadMultipleFilesChosen
136 );
137
138 // Disable the artificial dropdown (not inserted by React) via a class, as it can not use :disabled.
139 useEffect( () => {
140 appendReplaceDropdown.current.previousElementSibling.classList.toggle( 'disabled', appendReplaceDropdownDisabled );
141 }, [ appendReplaceDropdownDisabled ] );
142
143 return (
144 <table className="tablepress-postbox-table fixed">
145 <tbody>
146 <tr>
147 <th className="column-1" scope="row" id="import-source-header">
148 { __( 'Import Source', 'tablepress' ) }:
149 </th>
150 <td className="column-2">
151 {
152 Object.entries( importSources ) .map( ( [ importSource, importSourceData ] ) => (
153 <label
154 key={ importSource }
155 htmlFor={ `tables-import-source-${ importSource }` }
156 >
157 <input
158 name="import[source]"
159 id={ `tables-import-source-${ importSource }` }
160 type="radio"
161 aria-labelledby="import-source-header"
162 value={ importSource }
163 checked={ importSource === screenData.importSource }
164 onChange={ ( event ) => updateScreenData( { importSource: event.target.value } ) }
165 /> { importSourceData.label }
166 </label>
167 ) )
168 }
169 </td>
170 </tr>
171 <tr className="top-border bottom-border">
172 <th className="column-1 top-align" scope="row">
173 <label htmlFor={ `tables-import-${ screenData.importSource }` }>
174 { importSources[ screenData.importSource ].instruction }:
175 </label>
176 </th>
177 <td className="column-2">
178 {
179 /*
180 * Always add the "File Upload" UI to the DOM, but hide it using `style="display: none;"` below.
181 * This ensures that the <input type="file"> field works, as that is "uncontrolled" in React, and setting its value (files) is not possible.
182 */
183 }
184 <div
185 className="file-upload-area"
186 style={ {
187 display: ( 'file-upload' === screenData.importSource ) ? 'block' : 'none',
188 } }
189 >
190 <input
191 name="import_file_upload[]"
192 id="tables-import-file-upload"
193 type="file"
194 multiple
195 required={ 'file-upload' === screenData.importSource }
196 onChange={ ( event ) => ( event.target.files && updateScreenData( { importFileUpload: event.target.files } ) ) }
197 onDragEnter={ () => fileUploadDropzone.current.classList.add( 'dragover' ) }
198 onDragLeave={ () => fileUploadDropzone.current.classList.remove( 'dragover' ) }
199 />
200 <div
201 ref={ fileUploadDropzone }
202 className="dropzone"
203 >
204 <span>
205 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
206 { 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 ) }
207 </span>
208 {
209 [ ...screenData.importFileUpload ].map( ( file ) =>
210 <span key={ file.name }>{ file.name }</span>
211 )
212 }
213 </div>
214 </div>
215 { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
216 <input
217 type="url"
218 name="import[url]"
219 id="tables-import-url"
220 className="large-text code"
221 required={ true }
222 value={ screenData.importUrl }
223 onChange={ ( event ) => updateScreenData( { importUrl: event.target.value } ) }
224 />
225 }
226 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
227 <input
228 ref={ importServerInput }
229 type="text"
230 name="import[server]"
231 id="tables-import-server"
232 className="large-text code"
233 required={ true }
234 value={ screenData.importServer }
235 onChange={ ( event ) => updateScreenData( { importServer: event.target.value } ) }
236 />
237 }
238 { 'form-field' === screenData.importSource &&
239 <textarea
240 name="import[form-field]"
241 id="tables-import-form-field"
242 rows="15"
243 cols="40"
244 className="large-text code"
245 required={ true }
246 value={ screenData.importFormField }
247 onChange={ ( event ) => updateScreenData( { importFormField: event.target.value } ) }
248 />
249 }
250 { 'form-field' !== screenData.importSource &&
251 <p className="info-text" style={ {
252 marginTop: '0.5em',
253 } }>
254 <Icon icon={ info } />
255 <span>
256 { __( 'You can also import multiple tables by placing them in a ZIP file.', 'tablepress' ) }
257 </span>
258 </p>
259 }
260 </td>
261 </tr>
262 <tr className="top-border">
263 <th className="column-1" scope="row" id="import-type-header">
264 { __( 'Add, Replace, or Append?', 'tablepress' ) }:
265 </th>
266 <td className="column-2">
267 <label htmlFor="tables-import-type-add">
268 <input
269 name="import[type]"
270 id="tables-import-type-add"
271 type="radio"
272 aria-labelledby="import-type-header"
273 value="add"
274 checked={ 'add' === screenData.importType || 0 === tablesCount }
275 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
276 /> { __( 'Add as new table', 'tablepress' ) }
277 </label>
278 <label htmlFor="tables-import-type-replace">
279 <input
280 name="import[type]"
281 id="tables-import-type-replace"
282 type="radio"
283 aria-labelledby="import-type-header"
284 value="replace"
285 disabled={ 0 === tablesCount }
286 checked={ 'replace' === screenData.importType }
287 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
288 /> { __( 'Replace existing table', 'tablepress' ) }
289 </label>
290 <label htmlFor="tables-import-type-append">
291 <input
292 name="import[type]"
293 id="tables-import-type-append"
294 type="radio"
295 aria-labelledby="import-type-header"
296 value="append"
297 disabled={ 0 === tablesCount }
298 checked={ 'append' === screenData.importType }
299 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
300 /> { __( 'Append rows to existing table', 'tablepress' ) }
301 </label>
302 </td>
303 </tr>
304 <tr className="top-border bottom-border">
305 <th className="column-1" scope="row">
306 <label htmlFor="tables-import-existing-table">
307 { __( 'Table to replace or append to', 'tablepress' ) }:
308 </label>
309 </th>
310 <td className="column-2">
311 <select
312 ref={ appendReplaceDropdown }
313 id="tables-import-existing-table"
314 name="import[existing_table]"
315 disabled={ appendReplaceDropdownDisabled }
316 value={ screenData.importExistingTable }
317 >
318 <option value="">
319 {
320 ' ' // Use a space as an empty string will be printed as `&nbsp;` by jSuites.
321 }
322 </option>
323 { tablesSelectOptions }
324 </select>
325 </td>
326 </tr>
327 <tr className="top-border">
328 <td className="column-1"></td>
329 <td className="column-2">
330 <input
331 type="hidden"
332 name="import[legacy_import]"
333 value={ tp.import.legacyImport }
334 />
335 <input
336 type="submit"
337 value={ _x( 'Import', 'button', 'tablepress' ) }
338 className="button button-primary button-large"
339 id="import-submit-button"
340 onClick={ () => {
341 // Show validation :invalid CSS pseudo-selector highlighting.
342 updateScreenData( { validationHighlighting: true } );
343
344 // When importing from the server, the value must have been changed from the default (normally ABSPATH).
345 if ( 'server' === screenData.importSource && tp.import.importServer === screenData.importServer ) {
346 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
347 }
348
349 // If the table selection dropdown for replace or append is enabled, a table must be selected.
350 if ( ! appendReplaceDropdownDisabled && '' === screenData.importExistingTable ) {
351 // Use the jSuites dropdown input field, as the actual <select> is hidden.
352 appendReplaceDropdown.current.previousElementSibling.querySelector( '.jdropdown-header' ).setCustomValidity( __( 'You must select a table.', 'tablepress' ) );
353 }
354 } }
355 />
356 </td>
357 </tr>
358 </tbody>
359 </table>
360 );
361 };
362
363 export default Screen;
364