Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions dist/config-import-bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/config-import-bundle.js.map

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions dist/log.bundle-sizes.rest-import-ui.txt
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
@mui/icons-material: 5.93 MB (54.3%)
@mui/icons-material: 5.93 MB (54.2%)
@mui/material: 1.28 MB (11.7%)
@mui/base: 387.29 KB (3.46%)
lodash: 239.44 KB (2.14%)
core-js-pure: 172.67 KB (1.54%)
@mui/system: 161.76 KB (1.45%)
@mui/system: 161.76 KB (1.44%)
immutable: 139.01 KB (1.24%)
react-dom: 129.98 KB (1.16%)
js-yaml: 105.01 KB (0.938%)
axios: 87.01 KB (0.778%)
i18next: 82.68 KB (0.739%)
js-yaml: 105.01 KB (0.937%)
axios: 87.01 KB (0.777%)
i18next: 82.68 KB (0.738%)
@popperjs/core: 68.84 KB (0.615%)
buffer: 56.99 KB (0.509%)
react-transition-group: 55.52 KB (0.496%)
@emotion/cache: 40.02 KB (0.358%)
react-redux: 36.88 KB (0.330%)
@emotion/cache: 40.02 KB (0.357%)
react-redux: 36.88 KB (0.329%)
@mui/utils: 34.3 KB (0.306%)
react-i18next: 25.61 KB (0.229%)
x2js: 24.59 KB (0.220%)
x2js: 24.59 KB (0.219%)
stylis: 23.54 KB (0.210%)
@emotion/react: 23.27 KB (0.208%)
@emotion/styled: 19 KB (0.170%)
sha.js: 18.57 KB (0.166%)
redux: 16.37 KB (0.146%)
url-parse: 16.23 KB (0.145%)
@emotion/serialize: 13.35 KB (0.119%)
js-base64: 10.11 KB (0.0903%)
reselect: 8.85 KB (0.0791%)
js-base64: 10.11 KB (0.0902%)
reselect: 8.85 KB (0.0790%)
react: 7.98 KB (0.0713%)
redux-immutable: 5.43 KB (0.0485%)
process: 5.29 KB (0.0473%)
@emotion/sheet: 4.66 KB (0.0417%)
scheduler: 4.33 KB (0.0387%)
process: 5.29 KB (0.0472%)
@emotion/sheet: 4.66 KB (0.0416%)
scheduler: 4.33 KB (0.0386%)
@emotion/is-prop-valid: 4.33 KB (0.0386%)
deep-extend: 4.19 KB (0.0375%)
deep-extend: 4.19 KB (0.0374%)
base64-js: 3.84 KB (0.0343%)
@mui/styled-engine: 3.55 KB (0.0317%)
@mui/private-theming: 3.5 KB (0.0313%)
css.escape: 3.08 KB (0.0275%)
@babel/runtime-corejs3: 3 KB (0.0269%)
serialize-error: 2.93 KB (0.0262%)
@babel/runtime-corejs3: 3 KB (0.0268%)
serialize-error: 2.93 KB (0.0261%)
hoist-non-react-statics: 2.68 KB (0.0239%)
prop-types: 2.6 KB (0.0232%)
querystringify: 2.5 KB (0.0224%)
Expand All @@ -51,15 +51,15 @@ safe-buffer: 1.63 KB (0.0146%)
@emotion/hash: 1.62 KB (0.0144%)
clipboard-copy: 1.61 KB (0.0144%)
dom-helpers: 1.57 KB (0.0140%)
randombytes: 1.54 KB (0.0138%)
randombytes: 1.54 KB (0.0137%)
@braintree/sanitize-url: 1.38 KB (0.0123%)
use-sync-external-store: 1.28 KB (0.0115%)
@emotion/unitless: 924 B (0.00806%)
inherits: 753 B (0.00657%)
requires-port: 753 B (0.00657%)
@emotion/use-insertion-effect-with-fallbacks: 472 B (0.00412%)
@emotion/weak-memoize: 379 B (0.00331%)
@emotion/unitless: 924 B (0.00805%)
inherits: 753 B (0.00656%)
requires-port: 753 B (0.00656%)
@emotion/use-insertion-effect-with-fallbacks: 472 B (0.00411%)
@emotion/weak-memoize: 379 B (0.00330%)
clsx: 374 B (0.00326%)
void-elements: 338 B (0.00295%)
@emotion/memoize: 202 B (0.00176%)
<self>: 1.68 MB (15.4%)
<self>: 1.69 MB (15.5%)
6 changes: 3 additions & 3 deletions dist/rest-import-bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/rest-import-bundle.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wavemaker/rest-client-ui",
"version": "0.0.27",
"version": "0.0.28",
"private": false,
"main": "./dist/core/components/RestImport.js",
"release": {
Expand Down
36 changes: 22 additions & 14 deletions src/core/components/RestImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ProviderModal, { ProviderI } from './ProviderModal'
import { BodyParamsI, HeaderAndQueryTable, MultipartTable, HeaderAndQueryI, TableRowStyled, tableHeaderStyle, tableRowStyle } from './Table'
import {
retrievePathParamNamesFromURL, httpStatusCodes, isValidUrl, removeDuplicatesByComparison, constructUpdatedQueryString,
findDuplicatesByComparison, retrieveQueryDetailsFromURL, checkTypeForParameter
findDuplicatesByComparison, retrieveQueryDetailsFromURL, checkTypeForParameter, isValueValidForType
} from './common/common'
import InfoIcon from '@mui/icons-material/Info'
import { AxiosRequestConfig, AxiosResponse } from 'axios'
Expand Down Expand Up @@ -410,7 +410,7 @@ export default function RestImport({ language, restImportConfig }: { language: s
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// On mount, normalize multi/csv query params so the table and URL stay in sync after save/reopen.
// On mount, normalize multi-value query params so the table and URL stay in sync after save/reopen.
useEffect(() => {
// Ignore the trailing empty placeholder row (name === '').
const configQueryParams = restImportConfig?.queryParams?.filter((row) => row.name) || []
Expand All @@ -424,9 +424,9 @@ export default function RestImport({ language, restImportConfig }: { language: s
const hasRepeatedNames = configQueryParams.some(
(param) => configQueryParams.filter((row) => row.name === param.name).length > 1
)
// Only rebuild state when there is something special to preserve (multi/csv/repeats).
// Only rebuild state when there is something special to preserve (multi/repeats).
// For plain single-value params the incoming url/queryParams are already fine.
if (hasMultiFormat || hasRepeatedNames || configQueryParams.some((param) => param.collectionFormat === 'csv')) {
if (hasMultiFormat || hasRepeatedNames) {
// Backfill collectionFormat: 'multi' on repeated rows that don't carry the flag yet,
// so downstream URL building keeps them as separate key=value pairs (not comma-joined).
const normalizedParams = configQueryParams.map((param) => {
Expand Down Expand Up @@ -631,7 +631,13 @@ export default function RestImport({ language, restImportConfig }: { language: s
setqueryParams([{ name: '', value: '', type: 'string' }])
return
}
const updatedQueryParams = retrieveQueryDetailsFromURL(apiURL)
// Re-parsing the URL loses the type selected by the user, so preserve the existing
// type for parameters that are still present; use the parsed type for new parameters.
const existingTypeByName = new Map(queryParams.filter((param) => param.name).map((param) => [param.name, param.type]))
const updatedQueryParams = retrieveQueryDetailsFromURL(apiURL).map((param) => ({
...param,
type: existingTypeByName.get(param.name) ?? param.type
}))
if (updatedQueryParams.length > 0) {
// Repeated query names (type=a&type=b) are multi-value params — keep as separate rows.
const nameOccurrences = updatedQueryParams.reduce<Record<string, number>>((acc, param) => {
Expand Down Expand Up @@ -1135,7 +1141,7 @@ export default function RestImport({ language, restImportConfig }: { language: s
}
else if (param.in === 'query') {
// Detect multi-value query parameters and generate the appropriate
// Swagger array definition (collectionFormat: 'multi' or 'csv').
// Swagger array definition (collectionFormat: 'multi').
const matchingQueries = query.filter((q) => q.name === param.name && q.name !== '')
if (matchingQueries.length > 1 || matchingQueries[0]?.collectionFormat === 'multi') {
param.type = 'array'
Expand All @@ -1144,11 +1150,6 @@ export default function RestImport({ language, restImportConfig }: { language: s
setSwaggerParameters(param, matchingQueries[0], "QUERY")
} else if (matchingQueries.length === 1) {
setSwaggerParameters(param, matchingQueries[0], "QUERY")
if (matchingQueries[0].collectionFormat === 'csv') {
param.type = 'array'
param.collectionFormat = 'csv'
param.items = param.items || { type: 'string' }
}
}
}
else if (param.in === 'formData') {
Expand Down Expand Up @@ -1188,8 +1189,6 @@ export default function RestImport({ language, restImportConfig }: { language: s
function setSwaggerParameters(obj: any, param: HeaderAndQueryI, from: "HEADER" | "QUERY") {
const typeCheck = checkTypeForParameter(param.type)
if (typeCheck === 'SERVER') {
if (from === "HEADER")
obj.items.type = param.type
obj['x-WM-VARIABLE_KEY'] = param.type === 'DATETIME' ? "DATE_TIME" : param.type
obj["x-WM-VARIABLE_TYPE"] = "SERVER"
obj['format'] = param.type
Expand All @@ -1205,6 +1204,10 @@ export default function RestImport({ language, restImportConfig }: { language: s
obj["x-WM-EDITABLE"] = false
obj['format'] = param.type
}
// keep the array's element type in sync with the selected type (e.g. Integer with 2+ values)
if (obj.type === 'array' && obj.items) {
obj.items.type = param.type
}
}
return settingsUploadResponseData
}
Expand Down Expand Up @@ -1371,6 +1374,11 @@ export default function RestImport({ language, restImportConfig }: { language: s

}

// Dismissing the banner (its own "x") must only hide the message, not waive validation —
// the Test button stays disabled independently whenever a test value still doesn't match
// its declared type, regardless of whether alertMsg has been dismissed.
const hasInvalidTestValue = [...headerParams, ...queryParams].some(row => !isValueValidForType(row.value, row.type))

return (
<ThemeProvider theme={theme}>
<Stack sx={{ height: "97vh" }} className='rest-import-ui'>
Expand Down Expand Up @@ -1407,7 +1415,7 @@ export default function RestImport({ language, restImportConfig }: { language: s
getPathParams()
handleQueryChange()
}} className='url-input' name='wm-webservice-sample-url' autoFocus={true} value={apiURL} onChange={(e) => setapiURL(e.target.value.trim())} size='small' fullWidth />
<Button className='test-btn' name="wm-webservice-sample-test" onClick={handleTestClick} variant='contained' disabled={alertMsg}>{translate('TEST')}</Button>
<Button className='test-btn' name="wm-webservice-sample-test" onClick={handleTestClick} variant='contained' disabled={alertMsg || hasInvalidTestValue}>{translate('TEST')}</Button>
</Stack>
<Grid mt={2} container>
<Grid item md={4}>
Expand Down
76 changes: 69 additions & 7 deletions src/core/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import AddIcon from '@mui/icons-material/Add';
import {
constructUpdatedQueryString, findDuplicateObjectsWithinArray, findDuplicatesByComparison,
getCurrentDateTime
getCurrentDateTime, isPartialValueValidForType, isValueValidForType
} from './common/common';
import styled from "@emotion/styled";
import { FileUploadOutlined } from '@mui/icons-material';
Expand Down Expand Up @@ -171,21 +171,49 @@ export function HeaderAndQueryTable(
}
setValue(valueClone)
}
// Builds the "not a valid <Type> value" toast message, reusing the same label shown in the Type dropdown.
const notifyTypeMismatch = (data: HeaderAndQueryI) => {
const typeLabel = selectTypes.UITypes.find(t => t.value === data.type)?.label || data.type
handleToastError({ message: `"${data.value}" is not a valid ${typeLabel} value for parameter "${data.name}".`, type: 'error' })
}

// Checks both header/query tables for the first row whose value doesn't match its type.
// Returns the invalid row to update the banner, or undefined when all rows are valid.
const findTypeMismatch = (rows: HeaderAndQueryI[], otherRows: HeaderAndQueryI[]) => {
const result = [...rows, ...otherRows].find(row => !isValueValidForType(row.value, row.type))
return result
}

const handleChangeType = (event: SelectChangeEvent, indices: number[]) => {
// Apply the selected type to all rows belonging to the same query parameter.
// This ensures multi-value query parameters (e.g. ?id=1&id=2) remain consistent,
// then rebuild the URL from the updated query parameter state.
// apply the selected type to every row of this query param (multi-value params share one type)
const valueClone = [...value]
let mismatchedEntry: HeaderAndQueryI | undefined
indices.forEach((currentIndex) => {
const data = valueClone[currentIndex]
if (selectTypes.ServerSideProperties.find(e => e.value === event.target.value)) {
data.type = event.target.value
data.value = ServerSidePropertiesMap.get(event.target.value) as string
} else {
data.type = event.target.value
// an existing value may no longer match the newly selected type (e.g. "abc" under Integer)
if (!isValueValidForType(data.value, data.type)) {
mismatchedEntry = data
}
}
})
if (mismatchedEntry) {
notifyTypeMismatch(mismatchedEntry)
} else {
// Recheck both tables because this change may have resolved the current mismatch,
// but another invalid row may still require the banner.
const otherTableRows = from === 'query' ? headerParams : queryParams
const remainingMismatch = findTypeMismatch(valueClone, otherTableRows)
if (remainingMismatch) {
notifyTypeMismatch(remainingMismatch)
} else {
setAlertMsg(false)
}
}
if (from === 'query' && apiURL) {
changeapiURL(apiURL.split('?')[0] + constructUpdatedQueryString(valueClone))
}
Expand All @@ -194,12 +222,28 @@ export function HeaderAndQueryTable(


const handleChangeTestValue = (newValue: string, currentIndex: number) => {
// block only keystrokes that ADD invalid characters (e.g. a letter in an Integer field);
// always allow shrinking edits (backspace/delete) so an already-invalid value can be cleared
const currentType = value[currentIndex]?.type
const isGrowing = newValue.length > (value[currentIndex]?.value.length ?? 0)
if (isGrowing && currentType && !isPartialValueValidForType(newValue, currentType)) {
return
}
const valueClone = [...value]
valueClone.forEach((data, index) => {
if (index === currentIndex) {
data.value = newValue
}
})
// Revalidate both header and query tables after the value change so the banner is cleared only when all values are valid.
// If another mismatch remains, update the banner with the current invalid row instead of leaving a stale error message.
const otherTableRows = from === 'query' ? headerParams : queryParams
const remainingMismatch = findTypeMismatch(valueClone, otherTableRows)
if (remainingMismatch) {
notifyTypeMismatch(remainingMismatch)
} else {
setAlertMsg(false)
}
setValue(valueClone)
}

Expand Down Expand Up @@ -256,12 +300,25 @@ export function HeaderAndQueryTable(
}
}

// Removing a row can resolve the mismatch that triggered the banner, so revalidate both tables.
// Refresh the banner for any remaining mismatch, or clear it when all rows are valid.
const refreshAlertAfterRowRemoval = (valueClone: HeaderAndQueryI[]) => {
const otherTableRows = from === 'query' ? headerParams : queryParams
const remainingMismatch = findTypeMismatch(valueClone, otherTableRows)
if (remainingMismatch) {
notifyTypeMismatch(remainingMismatch)
} else {
setAlertMsg(false)
}
}

function handleDeleteRow(currentIndex: number) {
const valueClone = [...value]
valueClone.splice(currentIndex, 1)
if (from === 'query' && apiURL) {
changeapiURL(apiURL.split('?')[0] + constructUpdatedQueryString(valueClone))
}
refreshAlertAfterRowRemoval(valueClone)
setValue(valueClone)
}

Expand All @@ -270,6 +327,7 @@ export function HeaderAndQueryTable(
if (from === 'query' && apiURL) {
changeapiURL(apiURL.split('?')[0] + constructUpdatedQueryString(valueClone))
}
refreshAlertAfterRowRemoval(valueClone)
setValue(valueClone)
}

Expand All @@ -291,6 +349,10 @@ export function HeaderAndQueryTable(

const handleOnBlurTestValue = (currentIndex: number) => {
const valueClone = [...value]
const data = valueClone[currentIndex]
if (data && !isValueValidForType(data.value, data.type)) {
notifyTypeMismatch(data)
}
if (from === 'query') {
if (apiURL && currentIndex !== valueClone.length - 1) {
changeapiURL(apiURL.split('?')[0] + constructUpdatedQueryString(valueClone))
Expand Down Expand Up @@ -415,13 +477,13 @@ export function HeaderAndQueryTable(
className="wms wms-close"
aria-label="wm-webservice-remove-param-value"
onClick={() => handleDeleteRow(valueIndex)}
style={{ cursor: 'pointer', fontSize: 12 }} />
style={{ cursor: 'pointer', fontSize: 10 }} />
} : undefined} />
{/* fixed-width slot for "+", rendered (empty) on every row so every test-value
box reserves the same outer space and stays the same width */}
<Box sx={{ width: 14, flexShrink: 0, display: 'flex', justifyContent: 'center' }}>
{isLastValue && <i
className="wms wms-plus"
{isLastValue && <i
className="wms wms-plus"
aria-label="wm-webservice-add-param-value"
onClick={() => !isCurrentValueEmpty && handleAddValueToGroup(group.indices)}
style={{
Expand Down
Loading