PluginProbe
TablePress – Tables in WordPress made easy / 3.3
TablePress – Tables in WordPress made easy v3.3
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.3, at admin/js/import/screen.jsx

330 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 /**
11 * WordPress dependencies.
12 */
13 import { useEffect, useRef, useState } from 'react';
14 import {
15 Button,
16 __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
17 Icon,
18 RadioControl,
19 ComboboxControl,
20 Disabled,
21 TextareaControl,
22 TextControl,
23 } from '@wordpress/components';
24 import { info } from '@wordpress/icons';
25 import { __, _n, _x, sprintf } from '@wordpress/i18n';
26
27 // Details for the available import sources.
28 const importSources = {
29 'file-upload': {
30 label: __( 'File Upload', 'tablepress' ),
31 instruction: __( 'Select files', 'tablepress' ),
32 },
33 url: {
34 label: __( 'URL', 'tablepress' ),
35 instruction: __( 'File URL', 'tablepress' ),
36 },
37 server: {
38 label: __( 'File on server', 'tablepress' ),
39 instruction: __( 'Server Path to file', 'tablepress' ),
40 },
41 'form-field': {
42 label: __( 'Manual Input', 'tablepress' ),
43 instruction: __( 'Import data', 'tablepress' ),
44 },
45 };
46 if ( ! tp.import.showImportSourceUrl ) {
47 delete importSources.url;
48 }
49 if ( ! tp.import.showImportSourceServer ) {
50 delete importSources.server;
51 }
52
53 const importSourcesRadioOptions = Object.entries( importSources ).map( ( [ importSource, importSourceData ] ) => ( { value: importSource, label: importSourceData.label } ) );
54
55 // Number of tables.
56 const tablesCount = Object.keys( tp.import.tables ).length;
57
58 const tablesSelectOptions = Object.entries( tp.import.tables ).map( ( [ tableId, tableName ] ) => {
59 if ( '' === tableName.trim() ) {
60 tableName = __( '(no name)', 'tablepress' );
61 }
62 const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName );
63 return { value: tableId, label: optionText };
64 } );
65
66 // Custom component to conditionally disable its children, used for the ComboboxControl.
67 const ConditionalDisabled = ( { condition, children } ) => (
68 condition ? ( <Disabled>{ children }</Disabled> ) : children
69 );
70
71 /**
72 * Returns the "Import Screen" component's JSX markup.
73 *
74 * @return {Object} Import Screen component.
75 */
76 const Screen = () => {
77 const [ screenData, setScreenData ] = useState( {
78 importSource: tp.import.importSource,
79 importType: tp.import.importType,
80 importFileUpload: [],
81 importUrl: tp.import.importUrl,
82 importServer: tp.import.importServer,
83 importFormField: tp.import.importFormField,
84 importExistingTable: tp.import.importExistingTable,
85 validationHighlighting: false,
86 } );
87
88 /**
89 * Handles screen data state changes.
90 *
91 * @param {Object} updatedScreenData Data in the screen data state that should be updated.
92 */
93 const updateScreenData = ( updatedScreenData ) => {
94 setScreenData( ( currentScreenData ) => ( {
95 ...currentScreenData,
96 validationHighlighting: false, // Reset with every UI state change.
97 ...updatedScreenData,
98 } ) );
99 };
100
101 // References to DOM elements.
102 const importServerInput = useRef( null );
103 const fileUploadDropzone = useRef( null );
104
105 // Update the validation highlighting (using APIs and DOM elements outside of the React components) when the state changes.
106 useEffect( () => {
107 document.getElementById( 'tablepress_import-import-form' ).classList.toggle( 'no-validation-highlighting', ! screenData.validationHighlighting );
108 if ( ! screenData.validationHighlighting ) {
109 importServerInput.current?.setCustomValidity( '' );
110 // We need to use this dynamically generated ID by the ComboboxControl component. It does not (yet?) support a static ID or a ref.
111 document.getElementById( 'components-form-token-input-combobox-control-1' )?.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 return (
133 <table className="tablepress-postbox-table fixed">
134 <tbody>
135 <tr>
136 <th className="column-1" scope="row">
137 { __( 'Import Source', 'tablepress' ) }:
138 </th>
139 <td className="column-2">
140 <RadioControl
141 name="import[source]"
142 label={ __( 'Import Source', 'tablepress' ) }
143 hideLabelFromVision={ true }
144 selected={ screenData.importSource }
145 onChange={ ( importSource ) => updateScreenData( { importSource } ) }
146 options={ importSourcesRadioOptions }
147 />
148 </td>
149 </tr>
150 <tr className="top-border bottom-border">
151 <th className="column-1 top-align" scope="row">
152 <label htmlFor={ `tables-import-${ screenData.importSource }` }>
153 { importSources[ screenData.importSource ].instruction }:
154 </label>
155 </th>
156 <td className="column-2">
157 {
158 /*
159 * Always add the "File Upload" UI to the DOM, but hide it using `style="display: none;"` below.
160 * This ensures that the <input type="file"> field works, as that is "uncontrolled" in React, and setting its value (files) is not possible.
161 */
162 }
163 <div
164 className="file-upload-area"
165 style={ {
166 display: ( 'file-upload' === screenData.importSource ) ? 'block' : 'none',
167 } }
168 >
169 <input
170 name="import_file_upload[]"
171 id="tables-import-file-upload"
172 type="file"
173 multiple
174 required={ 'file-upload' === screenData.importSource }
175 onChange={ ( event ) => ( event.target.files && updateScreenData( { importFileUpload: event.target.files } ) ) }
176 onDragEnter={ () => fileUploadDropzone.current.classList.add( 'dragover' ) }
177 onDragLeave={ () => fileUploadDropzone.current.classList.remove( 'dragover' ) }
178 />
179 <div
180 ref={ fileUploadDropzone }
181 id="tables-import-file-upload-dropzone"
182 className="dropzone"
183 >
184 <span>
185 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
186 { 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 ) }
187 </span>
188 {
189 [ ...screenData.importFileUpload ].map( ( file ) =>
190 <span key={ file.name }>{ file.name }</span>
191 )
192 }
193 </div>
194 </div>
195 { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
196 <TextControl
197 __nextHasNoMarginBottom
198 __next40pxDefaultSize
199 type="url"
200 name="import[url]"
201 id="tables-import-url"
202 className="code"
203 required={ true }
204 value={ screenData.importUrl }
205 onChange={ ( importUrl ) => updateScreenData( { importUrl } ) }
206 />
207 }
208 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
209 <TextControl
210 __nextHasNoMarginBottom
211 __next40pxDefaultSize
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 __next40pxDefaultSize
276 id="tables-import-existing-table"
277 label={ __( 'Table to replace or append to', 'tablepress' ) }
278 hideLabelFromVision={ true }
279 placeholder={ __( '— Select or type —', 'tablepress' ) }
280 value={ screenData.importExistingTable }
281 options={ tablesSelectOptions }
282 onChange={ ( importExistingTable ) => updateScreenData( { importExistingTable } ) }
283 />
284 </ConditionalDisabled>
285 </td>
286 </tr>
287 <tr className="top-border">
288 <td className="column-1"></td>
289 <td className="column-2">
290 <input
291 // Send the legacy import flag to the server, so that it can handle the import accordingly.
292 type="hidden"
293 name="import[legacy_import]"
294 value={ tp.import.legacyImport }
295 />
296 <input
297 // 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.
298 type="hidden"
299 name="import[existing_table]"
300 value={ screenData.importExistingTable ?? '' }
301 />
302 <Button
303 variant="primary"
304 type="submit"
305 text={ _x( 'Import', 'button', 'tablepress' ) }
306 onClick={ () => {
307 // Show validation :invalid CSS pseudo-selector highlighting.
308 updateScreenData( { validationHighlighting: true } );
309
310 // When importing from the server, the value must have been changed from the default (normally ABSPATH).
311 if ( 'server' === screenData.importSource && tp.import.importServer === screenData.importServer ) {
312 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
313 }
314
315 // If the table selection dropdown for replace or append is enabled, a table must be selected.
316 if ( ! appendReplaceDropdownDisabled && ! screenData.importExistingTable ) {
317 // We need to use this dynamically generated ID by the ComboboxControl component. It does not (yet?) support a static ID or a ref.
318 document.getElementById( 'components-form-token-input-combobox-control-1' )?.setCustomValidity( __( 'You must select a table.', 'tablepress' ) );
319 }
320 } }
321 />
322 </td>
323 </tr>
324 </tbody>
325 </table>
326 );
327 };
328
329 export default Screen;
330