chore: add hasMany text field feature to ui (#5213)
* chore: add hasMany text field feature to ui * chore: remove bson dependency
This commit is contained in:
@@ -25,15 +25,15 @@
|
||||
"get-port": "5.1.1",
|
||||
"http-status": "1.6.2",
|
||||
"mongoose": "6.12.3",
|
||||
"mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"mongodb": "4.17.1",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
|
||||
"bson": "^6.3.0",
|
||||
"mongodb": "4.17.1",
|
||||
"mongodb-memory-server": "^9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
const hasMany = 'hasMany' in props ? props.hasMany : false
|
||||
const maxRows = 'maxRows' in props ? props.maxRows : Infinity
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const memoizedValidate = useCallback(
|
||||
(value, options) => {
|
||||
@@ -73,7 +73,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
|
||||
setValue(newVal)
|
||||
},
|
||||
[setValue],
|
||||
[onChangeFromProps, setValue],
|
||||
)
|
||||
|
||||
const [valueToRender, setValueToRender] = useState<
|
||||
@@ -148,17 +148,17 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
isCreatable
|
||||
isMulti
|
||||
isSortable
|
||||
// noOptionsMessage={({ inputValue }) => {
|
||||
// const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
||||
// if (isOverHasMany) {
|
||||
// return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
|
||||
// }
|
||||
// return null
|
||||
// }}
|
||||
noOptionsMessage={({ inputValue }) => {
|
||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
||||
if (isOverHasMany) {
|
||||
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
|
||||
}
|
||||
return null
|
||||
}}
|
||||
// numberOnly
|
||||
// onChange={handleHasManyChange}
|
||||
onChange={handleHasManyChange}
|
||||
options={[]}
|
||||
// placeholder={t('general:enterAValue')}
|
||||
placeholder={t('general:enterAValue')}
|
||||
showError={showError}
|
||||
value={valueToRender as Option[]}
|
||||
/>
|
||||
@@ -174,7 +174,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
onChange={handleChange}
|
||||
onWheel={(e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
e.target.blur()
|
||||
}}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import type { ClientValidate } from 'payload/types'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { Option } from '../../../elements/ReactSelect/types'
|
||||
import type { Props } from './types'
|
||||
|
||||
import ReactSelect from '../../../elements/ReactSelect'
|
||||
import { useConfig } from '../../../providers/Config'
|
||||
import { useLocale } from '../../../providers/Locale'
|
||||
import { useTranslation } from '../../../providers/Translation'
|
||||
@@ -45,7 +47,7 @@ const Text: React.FC<Props> = (props) => {
|
||||
|
||||
const Label = LabelFromProps || <LabelComp label={label} required={required} />
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
|
||||
@@ -71,9 +73,57 @@ const Text: React.FC<Props> = (props) => {
|
||||
localizationConfig: localizationConfig || undefined,
|
||||
})
|
||||
|
||||
const [valueToRender, setValueToRender] = useState<
|
||||
{ id: string; label: string; value: { value: number } }[]
|
||||
>([]) // Only for hasMany
|
||||
|
||||
const handleHasManyChange = useCallback(
|
||||
(selectedOption) => {
|
||||
if (!readOnly) {
|
||||
let newValue
|
||||
if (!selectedOption) {
|
||||
newValue = []
|
||||
} else if (Array.isArray(selectedOption)) {
|
||||
newValue = selectedOption.map((option) => option.value?.value || option.value)
|
||||
} else {
|
||||
newValue = [selectedOption.value?.value || selectedOption.value]
|
||||
}
|
||||
|
||||
setValue(newValue)
|
||||
}
|
||||
},
|
||||
[readOnly, setValue],
|
||||
)
|
||||
|
||||
// useEffect update valueToRender:
|
||||
useEffect(() => {
|
||||
if (hasMany && Array.isArray(value)) {
|
||||
setValueToRender(
|
||||
value.map((val, index) => {
|
||||
return {
|
||||
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
|
||||
label: `${val}`,
|
||||
value: {
|
||||
// React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
|
||||
toString: () => `${val}${index}`,
|
||||
value: val?.value || val,
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [value, hasMany])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
'number',
|
||||
className,
|
||||
showError && 'error',
|
||||
readOnly && 'read-only',
|
||||
hasMany && 'has-many',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
style={{
|
||||
@@ -83,24 +133,51 @@ const Text: React.FC<Props> = (props) => {
|
||||
>
|
||||
{Error}
|
||||
{Label}
|
||||
<div>
|
||||
{BeforeInput}
|
||||
<input
|
||||
data-rtl={renderRTL}
|
||||
{hasMany ? (
|
||||
<ReactSelect
|
||||
className={`field-${path.replace(/\./g, '__')}`}
|
||||
disabled={readOnly}
|
||||
id={`field-${path?.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
// prevent adding additional options if maxRows is reached
|
||||
filterOption={() =>
|
||||
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
|
||||
}
|
||||
isClearable
|
||||
isCreatable
|
||||
isMulti
|
||||
isSortable
|
||||
noOptionsMessage={() => {
|
||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
||||
if (isOverHasMany) {
|
||||
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
|
||||
}
|
||||
return null
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={(value as string) || ''}
|
||||
onChange={handleHasManyChange}
|
||||
options={[]}
|
||||
placeholder={t('general:enterAValue')}
|
||||
showError={showError}
|
||||
value={valueToRender as Option[]}
|
||||
/>
|
||||
{AfterInput}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{BeforeInput}
|
||||
<input
|
||||
data-rtl={renderRTL}
|
||||
disabled={readOnly}
|
||||
id={`field-${path?.replace(/\./g, '__')}`}
|
||||
name={path}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
{AfterInput}
|
||||
</div>
|
||||
)}
|
||||
{Description}
|
||||
</div>
|
||||
)
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -330,6 +330,9 @@ importers:
|
||||
mongoose:
|
||||
specifier: 6.12.3
|
||||
version: 6.12.3
|
||||
mongoose-aggregate-paginate-v2:
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6
|
||||
mongoose-paginate-v2:
|
||||
specifier: 1.7.22
|
||||
version: 1.7.22
|
||||
@@ -346,9 +349,6 @@ importers:
|
||||
'@types/mongoose-aggregate-paginate-v2':
|
||||
specifier: 1.0.9
|
||||
version: 1.0.9
|
||||
bson:
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0
|
||||
mongodb:
|
||||
specifier: 4.17.1
|
||||
version: 4.17.1
|
||||
@@ -6996,11 +6996,6 @@ packages:
|
||||
engines: {node: '>=14.20.1'}
|
||||
dev: true
|
||||
|
||||
/bson@6.3.0:
|
||||
resolution: {integrity: sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
dev: true
|
||||
|
||||
/buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: true
|
||||
@@ -12314,6 +12309,11 @@ packages:
|
||||
'@mongodb-js/saslprep': 1.1.4
|
||||
dev: true
|
||||
|
||||
/mongoose-aggregate-paginate-v2@1.0.6:
|
||||
resolution: {integrity: sha512-UuALu+mjhQa1K9lMQvjLL3vm3iALvNw8PQNIh2gp1b+tO5hUa0NC0Wf6/8QrT9PSJVTihXaD8hQVy3J4e0jO0Q==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/mongoose-paginate-v2@1.7.22:
|
||||
resolution: {integrity: sha512-xW5GugkE21DJiu9e13EOxKt4ejEKQkRP/S1PkkXRjnk2rRZVKBcld1nPV+VJ/YCPfm8hb3sz9OvI7O38RmixkA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
Reference in New Issue
Block a user