PluginProbe
TablePress – Tables in WordPress made easy / 2.4.4
TablePress – Tables in WordPress made easy v2.4.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 2.4.4 All 44 releases
tablepress / admin / js / import / screen.jsx

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

365 lines 12.4 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 id="tables-import-file-upload-dropzone"
203 className="dropzone"
204 >
205 <span>
206 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
207 { 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 ) }
208 </span>
209 {
210 [ ...screenData.importFileUpload ].map( ( file ) =>
211 <span key={ file.name }>{ file.name }</span>
212 )
213 }
214 </div>
215 </div>
216 { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
217 <input
218 type="url"
219 name="import[url]"
220 id="tables-import-url"
221 className="large-text code"
222 required={ true }
223 value={ screenData.importUrl }
224 onChange={ ( event ) => updateScreenData( { importUrl: event.target.value } ) }
225 />
226 }
227 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
228 <input
229 ref={ importServerInput }
230 type="text"
231 name="import[server]"
232 id="tables-import-server"
233 className="large-text code"
234 required={ true }
235 value={ screenData.importServer }
236 onChange={ ( event ) => updateScreenData( { importServer: event.target.value } ) }
237 />
238 }
239 { 'form-field' === screenData.importSource &&
240 <textarea
241 name="import[form-field]"
242 id="tables-import-form-field"
243 rows="15"
244 cols="40"
245 className="large-text code"
246 required={ true }
247 value={ screenData.importFormField }
248 onChange={ ( event ) => updateScreenData( { importFormField: event.target.value } ) }
249 />
250 }
251 { 'form-field' !== screenData.importSource &&
252 <p className="info-text" style={ {
253 marginTop: '0.5em',
254 } }>
255 <Icon icon={ info } />
256 <span>
257 { __( 'You can also import multiple tables by placing them in a ZIP file.', 'tablepress' ) }
258 </span>
259 </p>
260 }
261 </td>
262 </tr>
263 <tr className="top-border">
264 <th className="column-1" scope="row" id="import-type-header">
265 { __( 'Add, Replace, or Append?', 'tablepress' ) }:
266 </th>
267 <td className="column-2">
268 <label htmlFor="tables-import-type-add">
269 <input
270 name="import[type]"
271 id="tables-import-type-add"
272 type="radio"
273 aria-labelledby="import-type-header"
274 value="add"
275 checked={ 'add' === screenData.importType || 0 === tablesCount }
276 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
277 /> { __( 'Add as new table', 'tablepress' ) }
278 </label>
279 <label htmlFor="tables-import-type-replace">
280 <input
281 name="import[type]"
282 id="tables-import-type-replace"
283 type="radio"
284 aria-labelledby="import-type-header"
285 value="replace"
286 disabled={ 0 === tablesCount }
287 checked={ 'replace' === screenData.importType }
288 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
289 /> { __( 'Replace existing table', 'tablepress' ) }
290 </label>
291 <label htmlFor="tables-import-type-append">
292 <input
293 name="import[type]"
294 id="tables-import-type-append"
295 type="radio"
296 aria-labelledby="import-type-header"
297 value="append"
298 disabled={ 0 === tablesCount }
299 checked={ 'append' === screenData.importType }
300 onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
301 /> { __( 'Append rows to existing table', 'tablepress' ) }
302 </label>
303 </td>
304 </tr>
305 <tr className="top-border bottom-border">
306 <th className="column-1" scope="row">
307 <label htmlFor="tables-import-existing-table">
308 { __( 'Table to replace or append to', 'tablepress' ) }:
309 </label>
310 </th>
311 <td className="column-2">
312 <select
313 ref={ appendReplaceDropdown }
314 id="tables-import-existing-table"
315 name="import[existing_table]"
316 disabled={ appendReplaceDropdownDisabled }
317 value={ screenData.importExistingTable }
318 >
319 <option value="">
320 {
321 ' ' // Use a space as an empty string will be printed as `&nbsp;` by jSuites.
322 }
323 </option>
324 { tablesSelectOptions }
325 </select>
326 </td>
327 </tr>
328 <tr className="top-border">
329 <td className="column-1"></td>
330 <td className="column-2">
331 <input
332 type="hidden"
333 name="import[legacy_import]"
334 value={ tp.import.legacyImport }
335 />
336 <input
337 type="submit"
338 value={ _x( 'Import', 'button', 'tablepress' ) }
339 className="button button-primary button-large"
340 id="import-submit-button"
341 onClick={ () => {
342 // Show validation :invalid CSS pseudo-selector highlighting.
343 updateScreenData( { validationHighlighting: true } );
344
345 // When importing from the server, the value must have been changed from the default (normally ABSPATH).
346 if ( 'server' === screenData.importSource && tp.import.importServer === screenData.importServer ) {
347 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
348 }
349
350 // If the table selection dropdown for replace or append is enabled, a table must be selected.
351 if ( ! appendReplaceDropdownDisabled && '' === screenData.importExistingTable ) {
352 // Use the jSuites dropdown input field, as the actual <select> is hidden.
353 appendReplaceDropdown.current.previousElementSibling.querySelector( '.jdropdown-header' ).setCustomValidity( __( 'You must select a table.', 'tablepress' ) );
354 }
355 } }
356 />
357 </td>
358 </tr>
359 </tbody>
360 </table>
361 );
362 };
363
364 export default Screen;
365