| @@ -6,16 +6,19 @@ | ||
| 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 | -import { useState } from 'react'; | |
| 13 | +import { useEffect, useRef, useState } from 'react'; | |
| 16 | 14 | import { |
| 15 | + Button, | |
| 16 | + CheckboxControl, | |
| 17 | + __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis | |
| 17 | 18 | Icon, |
| 19 | + SelectControl, | |
| 20 | + __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis | |
| 18 | 21 | } from '@wordpress/components'; |
| 19 | 22 | import { info } from '@wordpress/icons'; |
| 20 | 23 | import { __, _x, sprintf } from '@wordpress/i18n'; |
| 21 | 24 | |
| @@ -26,24 +29,24 @@ | ||
| 26 | 29 | let exportTablesSelectSize = tablesCount + 1; |
| 27 | 30 | const maxExportTablesSelectSize = 12; |
| 28 | 31 | exportTablesSelectSize = Math.max( exportTablesSelectSize, 3 ); |
| 29 | 32 | exportTablesSelectSize = Math.min( exportTablesSelectSize, maxExportTablesSelectSize ); |
| 33 | +const exportTablesSelectMultiple = tp.export.zipSupportAvailable; | |
| 30 | 34 | |
| 31 | -// The <option> entries for the dropdowns do not depend on the state, so they can be created once. | |
| 32 | 35 | const tablesSelectOptions = Object.entries( tp.export.tables ).map( ( [ tableId, tableName ] ) => { |
| 33 | 36 | if ( '' === tableName.trim() ) { |
| 34 | 37 | tableName = __( '(no name)', 'tablepress' ); |
| 35 | 38 | } |
| 36 | 39 | const optionText = sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), tableId, tableName ); |
| 37 | - return <option key={ tableId } value={ tableId }>{ optionText }</option>; | |
| 40 | + return { value: tableId, label: optionText }; | |
| 38 | 41 | } ); |
| 39 | -const exportFormatsSelectOptions = Object.entries( tp.export.exportFormats ).map( ( [ exportFormat, exportFormatName ] ) => | |
| 40 | - <option key={ exportFormat } value={ exportFormat }>{ exportFormatName }</option> | |
| 41 | -); | |
| 42 | -const csvDelimitersSelectOptions = Object.entries( tp.export.csvDelimiters ).map( ( [ csvDelimiter, csvDelimiterName ] ) => | |
| 43 | - <option key={ csvDelimiter } value={ csvDelimiter }>{ csvDelimiterName }</option> | |
| 44 | -); | |
| 42 | +if ( ! exportTablesSelectMultiple ) { | |
| 43 | + tablesSelectOptions.unshift( { value: '', label: __( '— Select —', 'tablepress' ), disabled: true } ); | |
| 44 | +} | |
| 45 | 45 | |
| 46 | +const exportFormatsSelectOptions = Object.entries( tp.export.exportFormats ).map( ( [ exportFormat, exportFormatName ] ) => ( { value: exportFormat, label: exportFormatName } ) ); | |
| 47 | +const csvDelimitersSelectOptions = Object.entries( tp.export.csvDelimiters ).map( ( [ csvDelimiter, csvDelimiterName ] ) => ( { value: csvDelimiter, label: csvDelimiterName } ) ); | |
| 48 | + | |
| 46 | 49 | /** |
| 47 | 50 | * Returns the "Export Screen" component's JSX markup. |
| 48 | 51 | * |
| 49 | 52 | * @return {Object} Export Screen component. |
| @@ -48,8 +51,9 @@ | ||
| 48 | 51 | * |
| 49 | 52 | * @return {Object} Export Screen component. |
| 50 | 53 | */ |
| 51 | 54 | const Screen = () => { |
| 55 | + const tablesExportListSelect = useRef( null ); | |
| 52 | 56 | const [ screenData, setScreenData ] = useState( { |
| 53 | 57 | selectedTables: tp.export.selectedTables, |
| 54 | 58 | exportFormat: tp.export.exportFormat, |
| 55 | 59 | csvDelimiter: tp.export.csvDelimiter, |
| @@ -62,95 +66,94 @@ | ||
| 62 | 66 | |
| 63 | 67 | /** |
| 64 | 68 | * Handles screen data state changes. |
| 65 | 69 | * |
| 66 | - * @param {Object} updatedData Data in the screen data state that should be updated. | |
| 70 | + * @param {Object} updatedScreenData Data in the screen data state that should be updated. | |
| 67 | 71 | */ |
| 68 | - const updateScreenData = ( updatedData ) => { | |
| 69 | - const newScreenData = { | |
| 70 | - ...screenData, | |
| 71 | - ...updatedData, | |
| 72 | - }; | |
| 73 | - setScreenData( newScreenData ); | |
| 72 | + const updateScreenData = ( updatedScreenData ) => { | |
| 73 | + setScreenData( ( currentScreenData ) => ( { | |
| 74 | + ...currentScreenData, | |
| 75 | + ...updatedScreenData, | |
| 76 | + } ) ); | |
| 74 | 77 | }; |
| 75 | 78 | |
| 79 | + /* | |
| 80 | + * Set the size of the export tables dropdown to the number of tables, if ZIP file support is available. | |
| 81 | + * `SelectControl` does not support the HTML `size` attribute, so that this has to be done manually with an effect. | |
| 82 | + */ | |
| 83 | + useEffect( () => { | |
| 84 | + tablesExportListSelect.current.size = exportTablesSelectMultiple ? exportTablesSelectSize : 1; | |
| 85 | + }, [] ); | |
| 86 | + | |
| 76 | 87 | return ( |
| 77 | 88 | <table className="tablepress-postbox-table fixed"> |
| 78 | 89 | <tbody> |
| 79 | 90 | <tr> |
| 80 | 91 | <th className="column-1 top-align" scope="row"> |
| 81 | - <label htmlFor="tables-export-list"> | |
| 82 | - { __( 'Tables to Export', 'tablepress' ) }: | |
| 83 | - </label> | |
| 84 | - { tp.export.zipSupportAvailable && | |
| 85 | - <> | |
| 86 | - { | |
| 87 | - // Show a "Select all" checkbox to select all entries in the export tables dropdown. | |
| 88 | - } | |
| 89 | - <br /><br /> | |
| 90 | - <label htmlFor="tables-export-select-all"> | |
| 91 | - <input | |
| 92 | - type="checkbox" | |
| 93 | - id="tables-export-select-all" | |
| 92 | + <VStack | |
| 93 | + spacing="20px" | |
| 94 | + > | |
| 95 | + <label htmlFor="tables-export-list"> | |
| 96 | + { __( 'Tables to Export', 'tablepress' ) }: | |
| 97 | + </label> | |
| 98 | + { exportTablesSelectMultiple && | |
| 99 | + <VStack> | |
| 100 | + <CheckboxControl | |
| 101 | + // Show a "Select all" checkbox to select all entries in the export tables dropdown. | |
| 102 | + __nextHasNoMarginBottom | |
| 103 | + label={ __( 'Select all', 'tablepress' ) } | |
| 94 | 104 | checked={ screenData.selectedTables.length === tablesCount } |
| 95 | 105 | onChange={ () => { |
| 96 | 106 | const selectedTables = ( screenData.selectedTables.length === tablesCount ) ? [] : Object.keys( tp.export.tables ); |
| 97 | 107 | updateScreenData( { selectedTables } ); |
| 98 | 108 | } } |
| 99 | - /> { __( 'Select all', 'tablepress' ) } | |
| 100 | - </label> | |
| 101 | - { tablesCount > maxExportTablesSelectSize && | |
| 102 | - <> | |
| 103 | - { | |
| 109 | + /> | |
| 110 | + { tablesCount > maxExportTablesSelectSize && | |
| 111 | + <CheckboxControl | |
| 104 | 112 | // Show a "Reverse List" checkbox if more tables are shown than what the height of the export tables dropdown holds. |
| 105 | - } | |
| 106 | - <br /><br /> | |
| 107 | - <label htmlFor="tables-export-reverse-list"> | |
| 108 | - <input | |
| 109 | - id="tables-export-reverse-list" | |
| 110 | - type="checkbox" | |
| 111 | - checked={ screenData.reverseList } | |
| 112 | - onChange={ () => { | |
| 113 | - updateScreenData( { reverseList: ! screenData.reverseList } ); | |
| 114 | - tablesSelectOptions.reverse(); | |
| 115 | - } } | |
| 116 | - /> { __( 'Reverse list', 'tablepress' ) } | |
| 117 | - </label> | |
| 118 | - </> | |
| 119 | - } | |
| 120 | - </> | |
| 121 | - } | |
| 113 | + __nextHasNoMarginBottom | |
| 114 | + label={ __( 'Reverse list', 'tablepress' ) } | |
| 115 | + checked={ screenData.reverseList } | |
| 116 | + onChange={ ( reverseList ) => { | |
| 117 | + updateScreenData( { reverseList } ); | |
| 118 | + tablesSelectOptions.reverse(); | |
| 119 | + } } | |
| 120 | + /> | |
| 121 | + } | |
| 122 | + </VStack> | |
| 123 | + } | |
| 124 | + </VStack> | |
| 122 | 125 | </th> |
| 123 | 126 | <td className="column-2"> |
| 124 | - <select | |
| 127 | + <SelectControl | |
| 128 | + __nextHasNoMarginBottom | |
| 129 | + __next40pxDefaultSize | |
| 130 | + ref={ tablesExportListSelect } | |
| 125 | 131 | id="tables-export-list" |
| 126 | - size={ tp.export.zipSupportAvailable ? exportTablesSelectSize : 1 } | |
| 127 | - multiple={ tp.export.zipSupportAvailable } | |
| 128 | - value={ screenData.selectedTables } | |
| 129 | - onChange={ ( event ) => { | |
| 130 | - const selectedTables = [ ...event.target.selectedOptions ].map( ( option ) => option.value ); | |
| 132 | + // size={ exportTablesSelectMultiple ? exportTablesSelectSize : 1 } // Not supported by `SelectControl`, so done with an effect above. | |
| 133 | + multiple={ exportTablesSelectMultiple } | |
| 134 | + value={ exportTablesSelectMultiple ? screenData.selectedTables : ( screenData.selectedTables[0] ?? '' ) } | |
| 135 | + onChange={ ( selectedTables ) => { | |
| 136 | + if ( 'string' === typeof selectedTables ) { | |
| 137 | + selectedTables = [ selectedTables ]; | |
| 138 | + } | |
| 131 | 139 | updateScreenData( { selectedTables } ); |
| 132 | 140 | } } |
| 133 | - style={ { | |
| 134 | - width: '100%', | |
| 135 | - } } | |
| 136 | - > | |
| 137 | - { tablesSelectOptions } | |
| 138 | - </select> | |
| 139 | - { tp.export.zipSupportAvailable && | |
| 140 | - <> | |
| 141 | - <br /> | |
| 142 | - <p className="info-text"> | |
| 143 | - <Icon icon={ info } /> | |
| 144 | - <span> | |
| 145 | - { sprintf( | |
| 146 | - __( 'You can select multiple tables by holding down the “%1$s” key or the “%2$s” key for ranges.', 'tablepress' ), | |
| 147 | - window?.navigator?.platform?.includes( 'Mac' ) ? _x( '⌘', 'keyboard shortcut modifier key on a Mac keyboard', 'tablepress' ) : _x( 'Ctrl', 'keyboard key', 'tablepress' ), | |
| 148 | - _x( 'Shift', 'keyboard key', 'tablepress' ) ) | |
| 149 | - } | |
| 150 | - </span> | |
| 151 | - </p> | |
| 152 | - </> | |
| 141 | + options={ tablesSelectOptions } | |
| 142 | + /> | |
| 143 | + { exportTablesSelectMultiple && | |
| 144 | + <HStack | |
| 145 | + alignment="left" | |
| 146 | + > | |
| 147 | + <Icon icon={ info } /> | |
| 148 | + <span> | |
| 149 | + { sprintf( | |
| 150 | + __( 'You can select multiple tables by holding down the “%1$s” key or the “%2$s” key for ranges.', 'tablepress' ), | |
| 151 | + window?.navigator?.platform?.includes( 'Mac' ) ? _x( '⌘', 'keyboard shortcut modifier key on a Mac keyboard', 'tablepress' ) : _x( 'Ctrl', 'keyboard key', 'tablepress' ), | |
| 152 | + _x( 'Shift', 'keyboard key', 'tablepress' ) ) | |
| 153 | + } | |
| 154 | + </span> | |
| 155 | + </HStack> | |
| 153 | 156 | } |
| 154 | 157 | </td> |
| 155 | 158 | </tr> |
| 156 | 159 | <tr> |
| @@ -159,16 +162,21 @@ | ||
| 159 | 162 | { __( 'Export Format', 'tablepress' ) }: |
| 160 | 163 | </label> |
| 161 | 164 | </th> |
| 162 | 165 | <td className="column-2"> |
| 163 | - <select | |
| 164 | - id="tables-export-format" | |
| 165 | - name="export[format]" | |
| 166 | - value={ screenData.exportFormat } | |
| 167 | - onChange={ ( event ) => updateScreenData( { exportFormat: event.target.value } ) } | |
| 168 | - > | |
| 169 | - { exportFormatsSelectOptions } | |
| 170 | - </select> | |
| 166 | + <HStack> | |
| 167 | + <SelectControl | |
| 168 | + __nextHasNoMarginBottom | |
| 169 | + __next40pxDefaultSize | |
| 170 | + id="tables-export-format" | |
| 171 | + name="export[format]" | |
| 172 | + value={ screenData.exportFormat } | |
| 173 | + label={ __( 'Export Format', 'tablepress' ) } | |
| 174 | + hideLabelFromVision={ true } | |
| 175 | + onChange={ ( exportFormat ) => updateScreenData( { exportFormat } ) } | |
| 176 | + options={ exportFormatsSelectOptions } | |
| 177 | + /> | |
| 178 | + </HStack> | |
| 171 | 179 | </td> |
| 172 | 180 | </tr> |
| 173 | 181 | <tr> |
| 174 | 182 | <th className="column-1" scope="row"> |
| @@ -176,25 +184,29 @@ | ||
| 176 | 184 | { __( 'CSV Delimiter', 'tablepress' ) }: |
| 177 | 185 | </label> |
| 178 | 186 | </th> |
| 179 | 187 | <td className="column-2"> |
| 180 | - <select | |
| 181 | - id="tables-export-csv-delimiter" | |
| 182 | - name="export[csv_delimiter]" | |
| 183 | - disabled={ 'csv' !== screenData.exportFormat } | |
| 184 | - value={ screenData.csvDelimiter } | |
| 185 | - onChange={ ( event ) => updateScreenData( { csvDelimiter: event.target.value } ) } | |
| 188 | + <HStack | |
| 189 | + alignment="left" | |
| 186 | 190 | > |
| 187 | - { csvDelimitersSelectOptions } | |
| 188 | - </select> | |
| 189 | - { 'csv' !== screenData.exportFormat && | |
| 190 | - <> | |
| 191 | - { ' ' } | |
| 192 | - <span className="description"> | |
| 191 | + <SelectControl | |
| 192 | + __nextHasNoMarginBottom | |
| 193 | + __next40pxDefaultSize | |
| 194 | + id="tables-export-csv-delimiter" | |
| 195 | + name="export[csv_delimiter]" | |
| 196 | + value={ screenData.csvDelimiter } | |
| 197 | + label={ __( 'CSV Delimiter', 'tablepress' ) } | |
| 198 | + hideLabelFromVision={ true } | |
| 199 | + onChange={ ( csvDelimiter ) => updateScreenData( { csvDelimiter } ) } | |
| 200 | + options={ csvDelimitersSelectOptions } | |
| 201 | + disabled={ 'csv' !== screenData.exportFormat } | |
| 202 | + /> | |
| 203 | + { 'csv' !== screenData.exportFormat && | |
| 204 | + <span> | |
| 193 | 205 | { __( '(Only needed for CSV export.)', 'tablepress' ) } |
| 194 | 206 | </span> |
| 195 | - </> | |
| 196 | - } | |
| 207 | + } | |
| 208 | + </HStack> | |
| 197 | 209 | </td> |
| 198 | 210 | </tr> |
| 199 | 211 | <tr className="bottom-border"> |
| 200 | 212 | <th className="column-1" scope="row"> |
| @@ -201,25 +213,24 @@ | ||
| 201 | 213 | { __( 'ZIP file', 'tablepress' ) }: |
| 202 | 214 | </th> |
| 203 | 215 | <td className="column-2"> |
| 204 | 216 | { tp.export.zipSupportAvailable && |
| 205 | - <label htmlFor="tables-export-zip-file"> | |
| 206 | - <input | |
| 207 | - type="checkbox" | |
| 208 | - id="tables-export-zip-file" | |
| 217 | + <HStack | |
| 218 | + alignment="left" | |
| 219 | + > | |
| 220 | + <CheckboxControl | |
| 221 | + __nextHasNoMarginBottom | |
| 222 | + label={ __( 'Create a ZIP archive.', 'tablepress' ) } | |
| 209 | 223 | checked={ screenData.createZipFile || zipFileRequired } |
| 210 | 224 | disabled={ zipFileRequired } |
| 211 | - onChange={ () => updateScreenData( { createZipFile: ! screenData.createZipFile } ) } | |
| 212 | - /> { __( 'Create a ZIP archive.', 'tablepress' ) } | |
| 225 | + onChange={ ( createZipFile ) => updateScreenData( { createZipFile } ) } | |
| 226 | + /> | |
| 213 | 227 | { zipFileRequired && |
| 214 | - <> | |
| 215 | - { ' ' } | |
| 216 | - <span className="description"> | |
| 217 | - { __( '(Mandatory if more than one table is selected.)', 'tablepress' ) } | |
| 218 | - </span> | |
| 219 | - </> | |
| 228 | + <span> | |
| 229 | + { __( '(Mandatory if more than one table is selected.)', 'tablepress' ) } | |
| 230 | + </span> | |
| 220 | 231 | } |
| 221 | - </label> | |
| 232 | + </HStack> | |
| 222 | 233 | } |
| 223 | 234 | { ! tp.export.zipSupportAvailable && |
| 224 | 235 | __( 'Note: Support for ZIP file creation seems not to be available on this server.', 'tablepress' ) |
| 225 | 236 | } |
| @@ -228,22 +239,24 @@ | ||
| 228 | 239 | <tr className="top-border"> |
| 229 | 240 | <td className="column-1"></td> |
| 230 | 241 | <td className="column-2"> |
| 231 | 242 | <input |
| 243 | + // Send the list of tables to be exported as a string and not an array, to reduce potential issues with large arrays. | |
| 232 | 244 | type="hidden" |
| 233 | 245 | name="export[tables_list]" |
| 234 | 246 | value={ screenData.selectedTables.join() } |
| 235 | 247 | /> |
| 236 | 248 | <input |
| 249 | + // Send the ZIP file attribute as a hidden field, as disabled checkboxes are not sent in HTTP POST requests. | |
| 237 | 250 | type="hidden" |
| 238 | 251 | name="export[zip_file]" |
| 239 | 252 | value={ screenData.createZipFile || zipFileRequired } |
| 240 | 253 | /> |
| 241 | - <input | |
| 254 | + <Button | |
| 255 | + variant="primary" | |
| 242 | 256 | type="submit" |
| 243 | - value={ __( 'Download Export File', 'tablepress' ) } | |
| 244 | - className="button button-primary button-large" | |
| 245 | 257 | disabled={ 0 === screenData.selectedTables.length } |
| 258 | + text={ __( 'Download Export File', 'tablepress' ) } | |
| 246 | 259 | /> |
| 247 | 260 | </td> |
| 248 | 261 | </tr> |
| 249 | 262 | </tbody> |