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 +92 -126 2.4.2 → 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
@@ -44,20 +49,26 @@
44 49 if ( ! tp.import.showImportSourceServer ) {
45 50 delete importSources.server;
46 51 }
47 52
53 +const importSourcesRadioOptions = Object.entries( importSources ).map( ( [ importSource, importSourceData ] ) => ( { value: importSource, label: importSourceData.label } ) );
54 +
48 55 // Number of tables.
49 56 const tablesCount = Object.keys( tp.import.tables ).length;
50 57
51 -// The <option> entries for the dropdown do not depend on the state, so they can be created once.
52 58 const tablesSelectOptions = Object.entries( tp.import.tables ).map( ( [ tableId, tableName ] ) => {
53 59 if ( '' === tableName.trim() ) {
54 60 tableName = __( '(no name)', 'tablepress' );
55 61 }
56 62 const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName );
57 - return <option key={ tableId } value={ tableId }>{ optionText }</option>;
63 + return { value: tableId, label: optionText };
58 64 } );
59 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 +
60 71 /**
61 72 * Returns the "Import Screen" component's JSX markup.
62 73 *
63 74 * @return {Object} Import Screen component.
@@ -76,46 +87,29 @@
76 87
77 88 /**
78 89 * Handles screen data state changes.
79 90 *
80 - * @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.
81 92 */
82 - const updateScreenData = ( updatedData ) => {
83 - const newScreenData = {
84 - ...screenData,
93 + const updateScreenData = ( updatedScreenData ) => {
94 + setScreenData( ( currentScreenData ) => ( {
95 + ...currentScreenData,
85 96 validationHighlighting: false, // Reset with every UI state change.
86 - ...updatedData,
87 - };
88 - setScreenData( newScreenData );
97 + ...updatedScreenData,
98 + } ) );
89 99 };
90 100
91 101 // References to DOM elements.
92 102 const importServerInput = useRef( null );
93 - const appendReplaceDropdown = useRef( null );
94 103 const fileUploadDropzone = useRef( null );
95 104
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 105 // Update the validation highlighting (using APIs and DOM elements outside of the React components) when the state changes.
113 106 useEffect( () => {
114 107 document.getElementById( 'tablepress_import-import-form' ).classList.toggle( 'no-validation-highlighting', ! screenData.validationHighlighting );
115 108 if ( ! screenData.validationHighlighting ) {
116 109 importServerInput.current?.setCustomValidity( '' );
117 - 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( '' );
118 112 }
119 113 }, [ screenData.validationHighlighting ] );
120 114
121 115 // Determine calculated state variables to avoid repeating calculations.
@@ -134,39 +128,24 @@
134 128 || 'add' === screenData.importType
135 129 || fileUploadMultipleFilesChosen
136 130 );
137 131
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 132 return (
144 133 <table className="tablepress-postbox-table fixed">
145 134 <tbody>
146 135 <tr>
147 - <th className="column-1" scope="row" id="import-source-header">
136 + <th className="column-1" scope="row">
148 137 { __( 'Import Source', 'tablepress' ) }:
149 138 </th>
150 139 <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 - }
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 + />
169 148 </td>
170 149 </tr>
171 150 <tr className="top-border bottom-border">
172 151 <th className="column-1 top-align" scope="row">
@@ -198,8 +177,9 @@
198 177 onDragLeave={ () => fileUploadDropzone.current.classList.remove( 'dragover' ) }
199 178 />
200 179 <div
201 180 ref={ fileUploadDropzone }
181 + id="tables-import-file-upload-dropzone"
202 182 className="dropzone"
203 183 >
204 184 <span>
205 185 { 0 === screenData.importFileUpload.length && __( 'Click to select files, or drag them here.', 'tablepress' ) }
@@ -212,117 +192,97 @@
212 192 }
213 193 </div>
214 194 </div>
215 195 { tp.import.showImportSourceUrl && 'url' === screenData.importSource &&
216 - <input
196 + <TextControl
197 + __nextHasNoMarginBottom
198 + __next40pxDefaultSize
217 199 type="url"
218 200 name="import[url]"
219 201 id="tables-import-url"
220 - className="large-text code"
202 + className="code"
221 203 required={ true }
222 204 value={ screenData.importUrl }
223 - onChange={ ( event ) => updateScreenData( { importUrl: event.target.value } ) }
205 + onChange={ ( importUrl ) => updateScreenData( { importUrl } ) }
224 206 />
225 207 }
226 208 { tp.import.showImportSourceServer && 'server' === screenData.importSource &&
227 - <input
209 + <TextControl
210 + __nextHasNoMarginBottom
211 + __next40pxDefaultSize
228 212 ref={ importServerInput }
229 - type="text"
230 213 name="import[server]"
231 214 id="tables-import-server"
232 - className="large-text code"
215 + className="code"
233 216 required={ true }
234 217 value={ screenData.importServer }
235 - onChange={ ( event ) => updateScreenData( { importServer: event.target.value } ) }
218 + onChange={ ( importServer ) => updateScreenData( { importServer } ) }
236 219 />
237 220 }
238 221 { 'form-field' === screenData.importSource &&
239 - <textarea
222 + <TextareaControl
223 + __nextHasNoMarginBottom
240 224 name="import[form-field]"
241 225 id="tables-import-form-field"
242 226 rows="15"
243 227 cols="40"
244 - className="large-text code"
228 + className="code"
245 229 required={ true }
246 230 value={ screenData.importFormField }
247 - onChange={ ( event ) => updateScreenData( { importFormField: event.target.value } ) }
231 + onChange={ ( importFormField ) => updateScreenData( { importFormField } ) }
248 232 />
249 233 }
250 234 { 'form-field' !== screenData.importSource &&
251 - <p className="info-text" style={ {
252 - marginTop: '0.5em',
253 - } }>
235 + <HStack
236 + alignment="left"
237 + >
254 238 <Icon icon={ info } />
255 239 <span>
256 240 { __( 'You can also import multiple tables by placing them in a ZIP file.', 'tablepress' ) }
257 241 </span>
258 - </p>
242 + </HStack>
259 243 }
260 244 </td>
261 245 </tr>
262 246 <tr className="top-border">
263 - <th className="column-1" scope="row" id="import-type-header">
247 + <th className="column-1" scope="row">
264 248 { __( 'Add, Replace, or Append?', 'tablepress' ) }:
265 249 </th>
266 250 <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>
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 + />
302 263 </td>
303 264 </tr>
304 265 <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>
266 + <th className="column-1 top-align" scope="row">
267 + <label htmlFor="tables-import-existing-table">{ __( 'Table to replace or append to', 'tablepress' ) }:</label>
309 268 </th>
310 269 <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 }
270 + <ConditionalDisabled
271 + condition={ appendReplaceDropdownDisabled }
317 272 >
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>
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>
325 285 </td>
326 286 </tr>
327 287 <tr className="top-border">
328 288 <td className="column-1"></td>
@@ -327,17 +287,23 @@
327 287 <tr className="top-border">
328 288 <td className="column-1"></td>
329 289 <td className="column-2">
330 290 <input
291 + // Send the legacy import flag to the server, so that it can handle the import accordingly.
331 292 type="hidden"
332 293 name="import[legacy_import]"
333 294 value={ tp.import.legacyImport }
334 295 />
335 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"
336 304 type="submit"
337 - value={ _x( 'Import', 'button', 'tablepress' ) }
338 - className="button button-primary button-large"
339 - id="import-submit-button"
305 + text={ _x( 'Import', 'button', 'tablepress' ) }
340 306 onClick={ () => {
341 307 // Show validation :invalid CSS pseudo-selector highlighting.
342 308 updateScreenData( { validationHighlighting: true } );
343 309
@@ -346,11 +312,11 @@
346 312 importServerInput.current.setCustomValidity( __( 'You must specify a path to a file on the server.', 'tablepress' ) );
347 313 }
348 314
349 315 // 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' ) );
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' ) );
353 319 }
354 320 } }
355 321 />
356 322 </td>