PluginProbe
TablePress – Tables in WordPress made easy / 2.2.1
TablePress – Tables in WordPress made easy v2.2.1
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.js

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

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