PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
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
← All changes | admin/js/import/screen.jsx +96 -127 2.3.1 → 3.4 View file →
@@ -6,16 +6,21 @@
6 6 * @author Tobias Bäthge
7 7 * @since 2.2.0
8 8 */
9 9
10 -/* globals tp */
11 -
12 10 /**
13 11 * WordPress dependencies.
14 12 */
15 13 import { useEffect, useRef, useState } from 'react';
16 14 import {
15 + Button,
16 + __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
17 17 Icon,
18 + RadioControl,
19 + ComboboxControl,
20 + Disabled,
21 + TextareaControl,
22 + TextControl,
18 23 } from '@wordpress/components';
19 24 import { info } from '@wordpress/icons';
20 25 import { __, _n, _x, sprintf } from '@wordpress/i18n';
21 26
@@ -37,24 +42,33 @@
37 42 label: __( 'Manual Input', 'tablepress' ),
38 43 instruction: __( 'Import data', 'tablepress' ),
39 44 },
40 45 };
46 +if ( ! tp.import.showImportSourceUrl ) {
47 + delete importSources.url;
48 +}
41 49 if ( ! tp.import.showImportSourceServer ) {
42 50 delete importSources.server;
43 51 }
44 52
53 +const importSourcesRadioOptions = Object.entries( importSources ).map( ( [ importSource, importSourceData ] ) => ( { value: importSource, label: importSourceData.label } ) );
54 +
45 55 // Number of tables.
46 56 const tablesCount = Object.keys( tp.import.tables ).length;
47 57
48 -// The <option> entries for the dropdown do not depend on the state, so they can be created once.
49 58 const tablesSelectOptions = Object.entries( tp.import.tables ).map( ( [ tableId, tableName ] ) => {
50 59 if ( '' === tableName.trim() ) {
51 60 tableName = __( '(no name)', 'tablepress' );
52 61 }
53 62 const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName );
54 - return <option key={ tableId } value={ tableId }>{ optionText }</option>;
63 + return { value: tableId, label: optionText };
55 64 } );
56 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 +
57 71 /**
58 72 * Returns the "Import Screen" component's JSX markup.
59 73 *
60 74 * @return {Object} Import Screen component.
@@ -73,46 +87,29 @@
73 87
74 88 /**
75 89 * Handles screen data state changes.
76 90 *
77 - * @param {Object} updatedData Data in the screen data state that should be updated.
91 + * @param {Object} updatedScreenData Data in the screen data state that should be updated.
78 92 */
79 - const updateScreenData = ( updatedData ) => {
80 - const newScreenData = {
81 - ...screenData,
93 + const updateScreenData = ( updatedScreenData ) => {
94 + setScreenData( ( currentScreenData ) => ( {
95 + ...currentScreenData,
82 96 validationHighlighting: false, // Reset with every UI state change.
83 - ...updatedData,
84 - };
85 - setScreenData( newScreenData );
97 + ...updatedScreenData,
98 + } ) );
86 99 };
87 100
88 101 // References to DOM elements.
89 102 const importServerInput = useRef( null );
90 - const appendReplaceDropdown = useRef( null );
91 103 const fileUploadDropzone = useRef( null );
92 104
93 - // Initialize the jSuites dropdown when the component is mounted.
94 - useEffect( () => {
95 - jSuites.dropdown( appendReplaceDropdown.current, {
96 - autocomplete: true,
97 - placeholder: __( '— Select or type —', 'tablepress' ),
98 - onchange: ( element, index, oldValue, newValue ) => {
99 - // Directly update the state with an updater function, as the state is otherwise reset.
100 - setScreenData( ( newScreenData ) => ( {
101 - ...newScreenData,
102 - validationHighlighting: false,
103 - importExistingTable: newValue,
104 - } ) );
105 - },
106 - } );
107 - }, [] );
108 -
109 105 // Update the validation highlighting (using APIs and DOM elements outside of the React components) when the state changes.
110 106 useEffect( () => {
111 107 document.getElementById( 'tablepress_import-import-form' ).classList.toggle( 'no-validation-highlighting', ! screenData.validationHighlighting );
112 108 if ( ! screenData.validationHighlighting ) {
113 109 importServerInput.current?.setCustomValidity( '' );
114 - appendReplaceDropdown.current.previousElementSibling.querySelector( '.jdropdown-header' )?.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( '' );
115 112 }
116 113 }, [ screenData.validationHighlighting ] );
117 114
118 115 // Determine calculated state variables to avoid repeating calculations.
@@ -131,39 +128,24 @@
131 128 || 'add' === screenData.importType
132 129 || fileUploadMultipleFilesChosen
133 130 );
134 131
135 - // Disable the artificial dropdown (not inserted by React) via a class, as it can not use :disabled.
136 - useEffect( () => {
137 - appendReplaceDropdown.current.previousElementSibling.classList.toggle( 'disabled', appendReplaceDropdownDisabled );
138 - }, [ appendReplaceDropdownDisabled ] );
139 -
140 132 return (
141 133 <table className="tablepress-postbox-table fixed">
142 134 <tbody>
143 135 <tr>
144 - <th className="column-1" scope="row" id="import-source-header">
136 + <th className="column-1" scope="row">
145 137 { __( 'Import Source', 'tablepress' ) }:
146 138 </th>
147 139 <td className="column-2">
148 - {
149 - Object.entries( importSources ) .map( ( [ importSource, importSourceData ] ) => (
150 - <label
151 - key={ importSource }
152 - htmlFor={ `tables-import-source-${ importSource }` }
153 - >
154 - <input
155 - name="import[source]"
156 - id={ `tables-import-source-${ importSource }` }
157 - type="radio"
158 - aria-labelledby="import-source-header"
159 - value={ importSource }
160 - checked={ importSource === screenData.importSource }
161 - onChange={ ( event ) => updateScreenData( { importSource: event.target.value } ) }
162 - /> { importSourceData.label }
163 - </label>
164 - ) )
165 - }
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 + />
166 148 </td>
167 149 </tr>
168 150 <tr className="top-border bottom-border">
169 151 <th className="column-1 top-align" scope="row">
@@ -195,8 +177,9 @@
195 177 onDragLeave={ () => fileUploadDropzone.current.classList.remove( 'dragover' ) }
196 178 />
197 179 <div
198 180 ref={ fileUploadDropzone }
181 + id="tables-import-file-upload-dropzone"
199 182 className="dropzone"
200 183 >
201 184 <span>
202 185 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
@@ -208,118 +191,98 @@
208 191 )
209 192 }
210 193 </div>
211 194 </div>
212 - { 'url' === screenData.importSource &&
213 - <input
195 + { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
196 + <TextControl
197 + __nextHasNoMarginBottom
198 + __next40pxDefaultSize
214 199 type="url"
215 200 name="import[url]"
216 201 id="tables-import-url"
217 - className="large-text code"
202 + className="code"
218 203 required={ true }
219 204 value={ screenData.importUrl }
220 - onChange={ ( event ) => updateScreenData( { importUrl: event.target.value } ) }
205 + onChange={ ( importUrl ) => updateScreenData( { importUrl } ) }
221 206 />
222 207 }
223 208 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
224 - <input
209 + <TextControl
210 + __nextHasNoMarginBottom
211 + __next40pxDefaultSize
225 212 ref={ importServerInput }
226 - type="text"
227 213 name="import[server]"
228 214 id="tables-import-server"
229 - className="large-text code"
215 + className="code"
230 216 required={ true }
231 217 value={ screenData.importServer }
232 - onChange={ ( event ) => updateScreenData( { importServer: event.target.value } ) }
218 + onChange={ ( importServer ) => updateScreenData( { importServer } ) }
233 219 />
234 220 }
235 221 { 'form-field' === screenData.importSource &&
236 - <textarea
222 + <TextareaControl
223 + __nextHasNoMarginBottom
237 224 name="import[form-field]"
238 225 id="tables-import-form-field"
239 226 rows="15"
240 227 cols="40"
241 - className="large-text code"
228 + className="code"
242 229 required={ true }
243 230 value={ screenData.importFormField }
244 - onChange={ ( event ) => updateScreenData( { importFormField: event.target.value } ) }
231 + onChange={ ( importFormField ) => updateScreenData( { importFormField } ) }
245 232 />
246 233 }
247 234 { 'form-field' !== screenData.importSource &&
248 - <p className="info-text" style={ {
249 - marginTop: '0.5em',
250 - } }>
235 + <HStack
236 + alignment="left"
237 + >
251 238 <Icon icon={ info } />
252 239 <span>
253 240 { __( 'You can also import multiple tables by placing them in a ZIP file.', 'tablepress' ) }
254 241 </span>
255 - </p>
242 + </HStack>
256 243 }
257 244 </td>
258 245 </tr>
259 246 <tr className="top-border">
260 - <th className="column-1" scope="row" id="import-type-header">
247 + <th className="column-1" scope="row">
261 248 { __( 'Add, Replace, or Append?', 'tablepress' ) }:
262 249 </th>
263 250 <td className="column-2">
264 - <label htmlFor="tables-import-type-add">
265 - <input
266 - name="import[type]"
267 - id="tables-import-type-add"
268 - type="radio"
269 - aria-labelledby="import-type-header"
270 - value="add"
271 - checked={ 'add' === screenData.importType || 0 === tablesCount }
272 - onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
273 - /> { __( 'Add as new table', 'tablepress' ) }
274 - </label>
275 - <label htmlFor="tables-import-type-replace">
276 - <input
277 - name="import[type]"
278 - id="tables-import-type-replace"
279 - type="radio"
280 - aria-labelledby="import-type-header"
281 - value="replace"
282 - disabled={ 0 === tablesCount }
283 - checked={ 'replace' === screenData.importType }
284 - onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
285 - /> { __( 'Replace existing table', 'tablepress' ) }
286 - </label>
287 - <label htmlFor="tables-import-type-append">
288 - <input
289 - name="import[type]"
290 - id="tables-import-type-append"
291 - type="radio"
292 - aria-labelledby="import-type-header"
293 - value="append"
294 - disabled={ 0 === tablesCount }
295 - checked={ 'append' === screenData.importType }
296 - onChange={ ( event ) => updateScreenData( { importType: event.target.value } ) }
297 - /> { __( 'Append rows to existing table', 'tablepress' ) }
298 - </label>
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 + />
299 263 </td>
300 264 </tr>
301 265 <tr className="top-border bottom-border">
302 - <th className="column-1" scope="row">
303 - <label htmlFor="tables-import-existing-table">
304 - { __( 'Table to replace or append to', 'tablepress' ) }:
305 - </label>
266 + <th className="column-1 top-align" scope="row">
267 + <label htmlFor="tables-import-existing-table">{ __( 'Table to replace or append to', 'tablepress' ) }:</label>
306 268 </th>
307 269 <td className="column-2">
308 - <select
309 - ref={ appendReplaceDropdown }
310 - id="tables-import-existing-table"
311 - name="import[existing_table]"
312 - disabled={ appendReplaceDropdownDisabled }
313 - value={ screenData.importExistingTable }
270 + <ConditionalDisabled
271 + condition={ appendReplaceDropdownDisabled }
314 272 >
315 - <option value="">
316 - {
317 - ' ' // Use a space as an empty string will be printed as `&nbsp;` by jSuites.
318 - }
319 - </option>
320 - { tablesSelectOptions }
321 - </select>
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>
322 285 </td>
323 286 </tr>
324 287 <tr className="top-border">
325 288 <td className="column-1"></td>
@@ -324,17 +287,23 @@
324 287 <tr className="top-border">
325 288 <td className="column-1"></td>
326 289 <td className="column-2">
327 290 <input
291 + // Send the legacy import flag to the server, so that it can handle the import accordingly.
328 292 type="hidden"
329 293 name="import[legacy_import]"
330 294 value={ tp.import.legacyImport }
331 295 />
332 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"
333 304 type="submit"
334 - value={ _x( 'Import', 'button', 'tablepress' ) }
335 - className="button button-primary button-large"
336 - id="import-submit-button"
305 + text={ _x( 'Import', 'button', 'tablepress' ) }
337 306 onClick={ () => {
338 307 // Show validation :invalid CSS pseudo-selector highlighting.
339 308 updateScreenData( { validationHighlighting: true } );
340 309
@@ -343,11 +312,11 @@
343 312 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
344 313 }
345 314
346 315 // If the table selection dropdown for replace or append is enabled, a table must be selected.
347 - if ( ! appendReplaceDropdownDisabled && '' === screenData.importExistingTable ) {
348 - // Use the jSuites dropdown input field, as the actual <select> is hidden.
349 - appendReplaceDropdown.current.previousElementSibling.querySelector( '.jdropdown-header' ).setCustomValidity( __( 'You must select a table.', 'tablepress' ) );
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' ) );
350 319 }
351 320 } }
352 321 />
353 322 </td>