nitro-web 0.0.64 → 0.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/index.ts +5 -4
- package/components/partials/element/button.tsx +24 -14
- package/components/partials/element/filters.tsx +64 -56
- package/components/partials/form/field-color.tsx +11 -6
- package/components/partials/form/field-currency.tsx +1 -1
- package/components/partials/form/field-date.tsx +30 -14
- package/components/partials/form/field.tsx +12 -8
- package/components/partials/form/select.tsx +21 -16
- package/components/partials/styleguide.tsx +43 -27
- package/package.json +1 -1
- package/server/router.js +14 -13
- package/types/util.d.ts +69 -3
- package/types/util.d.ts.map +1 -1
- package/types.ts +3 -1
- package/util.js +126 -24
package/client/index.ts
CHANGED
|
@@ -37,19 +37,20 @@ export { Modal } from '../components/partials/element/modal'
|
|
|
37
37
|
export { Sidebar, type SidebarProps } from '../components/partials/element/sidebar'
|
|
38
38
|
export { Tooltip } from '../components/partials/element/tooltip'
|
|
39
39
|
export { Topbar } from '../components/partials/element/topbar'
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// Component Form Elements
|
|
41
42
|
export { Checkbox } from '../components/partials/form/checkbox'
|
|
42
43
|
export { Drop } from '../components/partials/form/drop'
|
|
43
44
|
export { DropHandler } from '../components/partials/form/drop-handler'
|
|
44
45
|
export { FormError } from '../components/partials/form/form-error'
|
|
45
|
-
export { Field, isFieldCached } from '../components/partials/form/field'
|
|
46
|
+
export { Field, isFieldCached, type FieldProps } from '../components/partials/form/field'
|
|
46
47
|
export { FieldColor, type FieldColorProps } from '../components/partials/form/field-color'
|
|
47
48
|
export { FieldCurrency, type FieldCurrencyProps } from '../components/partials/form/field-currency'
|
|
48
49
|
export { FieldDate, type FieldDateProps } from '../components/partials/form/field-date'
|
|
49
50
|
export { Location } from '../components/partials/form/location'
|
|
50
|
-
export { Select, getSelectStyle } from '../components/partials/form/select'
|
|
51
|
+
export { Select, getSelectStyle, type SelectProps } from '../components/partials/form/select'
|
|
51
52
|
|
|
52
|
-
// Component Other
|
|
53
|
+
// Component Other Components
|
|
53
54
|
export { IsFirstRender } from '../components/partials/is-first-render'
|
|
54
55
|
|
|
55
56
|
// Expose the injected config
|
|
@@ -11,6 +11,7 @@ type Button = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
11
11
|
IconLeftEnd?: React.ReactNode|'v'
|
|
12
12
|
IconRight?: React.ReactNode|'v'
|
|
13
13
|
IconRightEnd?: React.ReactNode|'v'
|
|
14
|
+
IconCenter?: React.ReactNode|'v'
|
|
14
15
|
children?: React.ReactNode|'v'
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -23,16 +24,18 @@ export function Button({
|
|
|
23
24
|
IconLeft,
|
|
24
25
|
IconLeftEnd,
|
|
25
26
|
IconRight,
|
|
26
|
-
IconRightEnd,
|
|
27
|
+
IconRightEnd,
|
|
28
|
+
IconCenter,
|
|
27
29
|
children,
|
|
28
30
|
type='button',
|
|
29
31
|
...props
|
|
30
32
|
}: Button) {
|
|
31
33
|
// const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
|
|
32
|
-
const iconPosition =
|
|
34
|
+
const iconPosition =
|
|
35
|
+
IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : IconCenter ? 'center' : 'none'
|
|
33
36
|
const base =
|
|
34
|
-
'relative inline-
|
|
35
|
-
'focus-visible:outline-offset-2 ring-inset ring-1'
|
|
37
|
+
'relative inline-flex items-center justify-center text-center font-medium shadow-sm focus-visible:outline ' +
|
|
38
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 ring-inset ring-1' + (children ? '' : ' aspect-square')
|
|
36
39
|
|
|
37
40
|
// Button colors, you can use custom colors by using className instead
|
|
38
41
|
const colors = {
|
|
@@ -46,14 +49,14 @@ export function Button({
|
|
|
46
49
|
|
|
47
50
|
// Button sizes (px is better for height consistency)
|
|
48
51
|
const sizes = {
|
|
49
|
-
xs: 'px-[6px]
|
|
50
|
-
sm: 'px-[10px]
|
|
51
|
-
md: 'px-[12px]
|
|
52
|
-
lg: 'px-[18px]
|
|
52
|
+
xs: 'px-[6px] h-[25px] px-button-x-xs h-button-h-xs text-xs !text-button-xs rounded',
|
|
53
|
+
sm: 'px-[10px] h-[32px] px-button-x-sm h-button-h-sm text-button-md text-button-sm rounded-md',
|
|
54
|
+
md: 'px-[12px] h-[38px] px-button-x-md h-button-h-md text-button-md rounded-md', // default
|
|
55
|
+
lg: 'px-[18px] h-[42px] px-button-x-lg h-button-h-lg text-button-md !text-button-lg rounded-md',
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
const appliedColor = color === 'custom' ? customColor : colors[color]
|
|
56
|
-
const contentLayout = `gap-x-1.5 ${iconPosition == 'none' ? '' : '
|
|
59
|
+
const contentLayout = `gap-x-1.5 ${iconPosition == 'none' ? '' : ''}`
|
|
57
60
|
const loading = isLoading ? '[&>*]:opacity-0 text-opacity-0' : ''
|
|
58
61
|
|
|
59
62
|
function getIcon(Icon: React.ReactNode | 'v') {
|
|
@@ -68,11 +71,18 @@ export function Button({
|
|
|
68
71
|
class={twMerge(`${base} ${sizes[size]} ${appliedColor} ${contentLayout} ${loading} nitro-button ${className||''}`)}
|
|
69
72
|
{...props}
|
|
70
73
|
>
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
{
|
|
75
|
+
IconCenter &&
|
|
76
|
+
<span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
77
|
+
{getIcon(IconCenter)}
|
|
78
|
+
</span>
|
|
79
|
+
}
|
|
80
|
+
{(IconLeft || IconLeftEnd) && getIcon(IconLeft || IconLeftEnd)}
|
|
81
|
+
<span class={`flex items-center ${iconPosition == 'leftEnd' || iconPosition == 'rightEnd' ? 'flex-1 justify-center' : ''}`}>
|
|
82
|
+
<span className="w-0"> </span> {/* for min-height */}
|
|
83
|
+
{children}
|
|
84
|
+
</span>
|
|
85
|
+
{(IconRight || IconRightEnd) && getIcon(IconRight || IconRightEnd)}
|
|
76
86
|
{
|
|
77
87
|
isLoading &&
|
|
78
88
|
<span className={
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { forwardRef, Dispatch, SetStateAction, useRef, useEffect, useImperativeHandle } from 'react'
|
|
2
|
-
import { Button, Dropdown, Field, Select,
|
|
3
|
-
import { camelCaseToTitle, debounce, omit, queryString, queryObject } from 'nitro-web/util'
|
|
2
|
+
import { Button, Dropdown, Field, Select, type FieldProps, type SelectProps } from 'nitro-web'
|
|
3
|
+
import { camelCaseToTitle, debounce, omit, queryString, queryObject, twMerge } from 'nitro-web/util'
|
|
4
4
|
import { ListFilterIcon } from 'lucide-react'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
name: string
|
|
8
|
-
type: 'text'|'date'|'search'|'select'
|
|
6
|
+
type CommonProps = {
|
|
9
7
|
label?: string
|
|
10
|
-
|
|
11
|
-
placeholder?: string
|
|
8
|
+
rowClassName?: string
|
|
12
9
|
}
|
|
10
|
+
export type FilterType = (
|
|
11
|
+
| FieldProps & CommonProps
|
|
12
|
+
| ({ type: 'select' } & SelectProps & CommonProps)
|
|
13
|
+
)
|
|
13
14
|
|
|
14
15
|
type FilterState = {
|
|
15
16
|
[key: string]: string | true
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
type FiltersProps = {
|
|
20
|
+
state?: FilterState
|
|
21
|
+
setState?: Dispatch<SetStateAction<FilterState>>
|
|
19
22
|
filters?: FilterType[]
|
|
20
|
-
state: FilterState
|
|
21
|
-
setState: Dispatch<SetStateAction<FilterState>>
|
|
22
23
|
elements?: {
|
|
23
24
|
Button?: typeof Button
|
|
24
25
|
Dropdown?: typeof Dropdown
|
|
@@ -31,19 +32,36 @@ type FiltersProps = {
|
|
|
31
32
|
buttonClassName?: string
|
|
32
33
|
buttonText?: string
|
|
33
34
|
buttonCounterClassName?: string
|
|
35
|
+
filtersContainerClassName?: string
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export type FiltersHandleType = {
|
|
37
39
|
submit: (includePagination?: boolean) => void
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
const debounceTime = 250
|
|
43
|
+
|
|
40
44
|
export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
41
|
-
filters,
|
|
45
|
+
filters,
|
|
46
|
+
setState: setState2,
|
|
47
|
+
state: state2,
|
|
48
|
+
buttonClassName,
|
|
49
|
+
buttonCounterClassName,
|
|
50
|
+
buttonProps,
|
|
51
|
+
buttonText,
|
|
52
|
+
dropdownProps,
|
|
53
|
+
elements,
|
|
54
|
+
filtersContainerClassName,
|
|
42
55
|
}, ref) => {
|
|
43
56
|
const location = useLocation()
|
|
57
|
+
const navigate = useNavigate()
|
|
58
|
+
const [lastUpdated, setLastUpdated] = useState(0)
|
|
59
|
+
const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
|
|
60
|
+
const [state3, setState3] = useState(() => ({ ...queryObject(location.search) }))
|
|
61
|
+
const [state, setState] = [state2 || state3, setState2 || setState3]
|
|
44
62
|
const stateRef = useRef(state)
|
|
45
|
-
const [
|
|
46
|
-
|
|
63
|
+
const count = useMemo(() => Object.keys(state).filter((k) => state[k] && filters?.some((f) => f.name === k)).length, [state, filters])
|
|
64
|
+
|
|
47
65
|
const Elements = {
|
|
48
66
|
Button: elements?.Button || Button,
|
|
49
67
|
Dropdown: elements?.Dropdown || Dropdown,
|
|
@@ -65,37 +83,43 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
|
65
83
|
}, [state])
|
|
66
84
|
|
|
67
85
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
// Only update the state if the filters haven't been input changed in the last 500ms
|
|
87
|
+
if (Date.now() - lastUpdated > (debounceTime + 250)) {
|
|
88
|
+
setState(() => ({
|
|
89
|
+
...queryObject(location.search),
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
71
92
|
}, [location.search])
|
|
72
93
|
|
|
73
94
|
function reset(e: React.MouseEvent<HTMLAnchorElement>, filter: FilterType) {
|
|
74
95
|
e.preventDefault()
|
|
75
96
|
setState((s) => omit(s, [filter.name]) as FilterState)
|
|
76
|
-
|
|
97
|
+
onAfterChange()
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
|
|
80
101
|
e.preventDefault()
|
|
81
|
-
setState((s) => (
|
|
82
|
-
|
|
83
|
-
} as FilterState))
|
|
84
|
-
debouncedSubmit()
|
|
102
|
+
setState((s) => omit(s, filters?.map((f) => f.name) || []) as FilterState)
|
|
103
|
+
onAfterChange()
|
|
85
104
|
}
|
|
86
105
|
|
|
87
|
-
async function
|
|
106
|
+
async function onInputChange(e: {target: {name: string, value: unknown}}) {
|
|
88
107
|
await onChange(setState, e)
|
|
108
|
+
onAfterChange()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onAfterChange() {
|
|
112
|
+
setLastUpdated(Date.now())
|
|
89
113
|
debouncedSubmit()
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
//
|
|
116
|
+
// Update the URL by replacing the current entry in the history stack
|
|
93
117
|
function submit(includePagination?: boolean) {
|
|
94
118
|
const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
|
|
95
|
-
|
|
119
|
+
navigate(location.pathname + queryStr, { replace: true })
|
|
96
120
|
}
|
|
97
|
-
|
|
98
|
-
if (!filters) return null
|
|
121
|
+
|
|
122
|
+
// if (!filters) return null
|
|
99
123
|
return (
|
|
100
124
|
<Elements.Dropdown
|
|
101
125
|
dir="bottom-right"
|
|
@@ -107,47 +131,31 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
|
107
131
|
<div class="text-lg font-semibold">Filters</div>
|
|
108
132
|
<Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
|
|
109
133
|
</div>
|
|
110
|
-
<div class=
|
|
134
|
+
<div class={twMerge(`flex flex-wrap gap-4 px-4 py-4 pb-6 ${filtersContainerClassName || ''}`)}>
|
|
111
135
|
{
|
|
112
|
-
filters
|
|
113
|
-
<div key={
|
|
136
|
+
filters?.map(({label, rowClassName, ...filter}, i) => (
|
|
137
|
+
<div key={i} class={twMerge(`w-full ${rowClassName||''}`)}>
|
|
114
138
|
<div class="flex justify-between">
|
|
115
|
-
<label for={filter.name}>{
|
|
139
|
+
<label for={filter.id || filter.name}>{label || camelCaseToTitle(filter.name)}</label>
|
|
116
140
|
<a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
|
|
117
141
|
</div>
|
|
118
142
|
{
|
|
119
|
-
|
|
120
|
-
<Elements.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
type={filter.type}
|
|
124
|
-
placeholder={filter.placeholder}
|
|
125
|
-
state={state}
|
|
126
|
-
onChange={_onChange}
|
|
127
|
-
/>
|
|
128
|
-
}
|
|
129
|
-
{
|
|
130
|
-
filter.type === 'date' &&
|
|
131
|
-
<Elements.Field
|
|
132
|
-
class="mb-4"
|
|
133
|
-
name={filter.name}
|
|
134
|
-
type="date"
|
|
135
|
-
mode="range"
|
|
143
|
+
filter.type === 'select' &&
|
|
144
|
+
<Elements.Select
|
|
145
|
+
{...filter}
|
|
146
|
+
class="mb-0"
|
|
136
147
|
state={state}
|
|
137
|
-
onChange={
|
|
138
|
-
|
|
148
|
+
onChange={onInputChange}
|
|
149
|
+
type={undefined}
|
|
139
150
|
/>
|
|
140
151
|
}
|
|
141
152
|
{
|
|
142
|
-
filter.type
|
|
143
|
-
<Elements.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
type="country"
|
|
153
|
+
filter.type !== 'select' &&
|
|
154
|
+
<Elements.Field
|
|
155
|
+
{...filter}
|
|
156
|
+
class="mb-0"
|
|
147
157
|
state={state}
|
|
148
|
-
|
|
149
|
-
onChange={_onChange}
|
|
150
|
-
placeholder={filter.placeholder}
|
|
158
|
+
onChange={onInputChange}
|
|
151
159
|
/>
|
|
152
160
|
}
|
|
153
161
|
</div>
|
|
@@ -3,24 +3,29 @@ import Saturation from '@uiw/react-color-saturation'
|
|
|
3
3
|
import Hue from '@uiw/react-color-hue'
|
|
4
4
|
import { Dropdown, util } from 'nitro-web'
|
|
5
5
|
|
|
6
|
-
export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
6
|
+
export type FieldColorProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'|'value'> & {
|
|
7
7
|
name: string
|
|
8
8
|
/** name is applied if id is not provided */
|
|
9
9
|
id?: string
|
|
10
10
|
defaultColor?: string
|
|
11
11
|
Icon?: React.ReactNode
|
|
12
|
-
onChange?: (event: { target: { name: string, value: string
|
|
13
|
-
value?: string
|
|
12
|
+
onChange?: (event: { target: { name: string, value: string } }) => void
|
|
13
|
+
value?: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function FieldColor({ defaultColor='#333', Icon, onChange, value, ...props }: FieldColorProps) {
|
|
16
|
+
export function FieldColor({ defaultColor='#333', Icon, onChange: onChangeProp, value: valueProp, ...props }: FieldColorProps) {
|
|
17
17
|
const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
|
|
18
18
|
const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
19
19
|
const id = props.id || props.name
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Since value and onChange are optional, we need to hold the value in state if not provided
|
|
22
|
+
const [internalValue, setInternalValue] = useState(valueProp ?? defaultColor)
|
|
23
|
+
const value = valueProp ?? internalValue
|
|
24
|
+
const onChange = onChangeProp ?? ((e: { target: { name: string, value: string } }) => setInternalValue(e.target.value))
|
|
25
|
+
|
|
26
|
+
function onInputChange(e: { target: { name: string, value: string } }) {
|
|
22
27
|
setLastChanged(`ic-${Date.now()}`)
|
|
23
|
-
|
|
28
|
+
onChange(e)
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
return (
|
|
@@ -149,7 +149,7 @@ export function FieldCurrency({ config, currency='nzd', onChange, value, default
|
|
|
149
149
|
defaultValue={defaultValue}
|
|
150
150
|
/>
|
|
151
151
|
<span
|
|
152
|
-
class={`absolute top-0 bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-input-
|
|
152
|
+
class={`absolute top-0 bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-input-base ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
|
|
153
153
|
>
|
|
154
154
|
{settings.prefix || settings.suffix}
|
|
155
155
|
</span>
|
|
@@ -9,14 +9,21 @@ type DropdownRef = {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
type PreFieldDateProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
|
12
|
+
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
|
|
12
13
|
name: string
|
|
14
|
+
/** mode of the date picker */
|
|
13
15
|
mode: Mode
|
|
14
|
-
|
|
16
|
+
/** name is used as the id if not provided */
|
|
15
17
|
id?: string
|
|
18
|
+
/** show the time picker */
|
|
16
19
|
showTime?: boolean
|
|
20
|
+
/** prefix to add to the input */
|
|
17
21
|
prefix?: string
|
|
22
|
+
/** number of months to show in the dropdown */
|
|
18
23
|
numberOfMonths?: number
|
|
24
|
+
/** icon to show in the input */
|
|
19
25
|
Icon?: React.ReactNode
|
|
26
|
+
/** direction of the dropdown */
|
|
20
27
|
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
21
28
|
}
|
|
22
29
|
|
|
@@ -27,7 +34,7 @@ export type FieldDateProps = (
|
|
|
27
34
|
value?: null|number|string
|
|
28
35
|
})
|
|
29
36
|
| ({ mode: 'multiple' | 'range' } & PreFieldDateProps & {
|
|
30
|
-
onChange
|
|
37
|
+
onChange?: (e: { target: { name: string, value: (null|number)[] } }) => void
|
|
31
38
|
value?: null|number|string|(null|number|string)[]
|
|
32
39
|
})
|
|
33
40
|
)
|
|
@@ -37,8 +44,16 @@ type TimePickerProps = {
|
|
|
37
44
|
onChange: (mode: Mode, value: number|null) => void
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
export function FieldDate({
|
|
41
|
-
|
|
47
|
+
export function FieldDate({
|
|
48
|
+
dir = 'bottom-left',
|
|
49
|
+
Icon,
|
|
50
|
+
mode,
|
|
51
|
+
numberOfMonths,
|
|
52
|
+
onChange: onChangeProp,
|
|
53
|
+
prefix = '',
|
|
54
|
+
showTime,
|
|
55
|
+
value: valueProp,
|
|
56
|
+
...props
|
|
42
57
|
}: FieldDateProps) {
|
|
43
58
|
const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
|
|
44
59
|
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
@@ -46,7 +61,12 @@ export function FieldDate({
|
|
|
46
61
|
const [month, setMonth] = useState<number|undefined>()
|
|
47
62
|
const [lastUpdated, setLastUpdated] = useState(0)
|
|
48
63
|
const id = props.id || props.name
|
|
49
|
-
|
|
64
|
+
|
|
65
|
+
// Since value and onChange are optional, we need to hold the value in state if not provided
|
|
66
|
+
const [internalValue, setInternalValue] = useState<typeof valueProp>(valueProp)
|
|
67
|
+
const value = valueProp ?? internalValue
|
|
68
|
+
const onChange = onChangeProp ?? ((e: { target: { name: string, value: any } }) => setInternalValue(e.target.value))
|
|
69
|
+
|
|
50
70
|
// Convert the value to an array of valid* dates
|
|
51
71
|
const dates = useMemo(() => {
|
|
52
72
|
const _dates = Array.isArray(value) ? value : [value]
|
|
@@ -70,10 +90,8 @@ export function FieldDate({
|
|
|
70
90
|
if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
71
91
|
setInputValue(getInputValue(value))
|
|
72
92
|
// Update the value
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
setLastUpdated(new Date().getTime())
|
|
76
|
-
}
|
|
93
|
+
onChange({ target: { name: props.name, value: value as any } })
|
|
94
|
+
setLastUpdated(new Date().getTime())
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
|
|
@@ -104,10 +122,8 @@ export function FieldDate({
|
|
|
104
122
|
|
|
105
123
|
// Update the value
|
|
106
124
|
const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
setLastUpdated(new Date().getTime())
|
|
110
|
-
}
|
|
125
|
+
onChange({ target: { name: props.name, value: value as any }})
|
|
126
|
+
setLastUpdated(new Date().getTime())
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
return (
|
|
@@ -135,7 +151,7 @@ export function FieldDate({
|
|
|
135
151
|
{
|
|
136
152
|
prefix &&
|
|
137
153
|
// Similar classNames to the input.tsx:IconWrapper()
|
|
138
|
-
<span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-
|
|
154
|
+
<span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-3">
|
|
139
155
|
{prefix}
|
|
140
156
|
</span>
|
|
141
157
|
}
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// Maybe use fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
2
3
|
import { css } from 'twin.macro'
|
|
3
4
|
import { FieldCurrency, FieldCurrencyProps, FieldColor, FieldColorProps, FieldDate, FieldDateProps } from 'nitro-web'
|
|
4
5
|
import { twMerge, getErrorFromState, deepFind } from 'nitro-web/util'
|
|
5
6
|
import { Errors, type Error } from 'nitro-web/types'
|
|
6
7
|
import { EnvelopeIcon, CalendarIcon, FunnelIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/20/solid'
|
|
7
8
|
import { memo } from 'react'
|
|
8
|
-
// Maybe use fill-current tw class for lucide icons (https://github.com/lucide-icons/lucide/discussions/458)
|
|
9
9
|
|
|
10
|
+
type FieldType = 'text' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color'
|
|
10
11
|
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
|
11
12
|
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
12
13
|
type FieldExtraProps = {
|
|
13
|
-
|
|
14
|
+
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
|
|
14
15
|
name: string
|
|
15
|
-
|
|
16
|
+
/** name is applied if id is not provided */
|
|
16
17
|
id?: string
|
|
17
|
-
|
|
18
|
+
/** state object to get the value, and check errors against */
|
|
18
19
|
state?: { errors?: Errors, [key: string]: any }
|
|
19
|
-
type
|
|
20
|
+
/** type of the field */
|
|
21
|
+
type?: FieldType
|
|
22
|
+
/** icon to show in the input */
|
|
20
23
|
icon?: React.ReactNode
|
|
21
24
|
iconPos?: 'left' | 'right'
|
|
22
|
-
/** Pass dependencies to break memoization, handy for onChange/onInputChange
|
|
25
|
+
/** Pass dependencies to break memoization, handy for onChange/onInputChange */
|
|
23
26
|
deps?: unknown[]
|
|
27
|
+
placeholder?: string
|
|
24
28
|
}
|
|
25
29
|
type IconWrapperProps = {
|
|
26
30
|
iconPos: string
|
|
@@ -46,7 +50,7 @@ export const Field = memo(FieldBase, (prev, next) => {
|
|
|
46
50
|
})
|
|
47
51
|
|
|
48
52
|
function FieldBase({ state, icon, iconPos: ip, ...props }: FieldProps) {
|
|
49
|
-
// type must be kept as props.type for TS to be happy and follow the conditions below
|
|
53
|
+
// `type` must be kept as props.type for TS to be happy and follow the conditions below
|
|
50
54
|
let value!: string
|
|
51
55
|
let Icon!: React.ReactNode
|
|
52
56
|
const error = getErrorFromState(state, props.name)
|
|
@@ -145,7 +149,7 @@ function getInputClasses({ error, Icon, iconPos, type }: { error?: Error, Icon?:
|
|
|
145
149
|
const plWithIcon = type == 'color' ? 'pl-9' : 'pl-8' // was sm:pl-8 pl-8, etc
|
|
146
150
|
const prWithIcon = type == 'color' ? 'pr-9' : 'pr-8'
|
|
147
151
|
return (
|
|
148
|
-
`block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-input-
|
|
152
|
+
`block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-input-base outline outline-1 -outline-offset-1 ` +
|
|
149
153
|
'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
|
|
150
154
|
(iconPos == 'right' && Icon ? `${pl} ${prWithIcon} ` : (Icon ? `${plWithIcon} ${pr} ` : `${pl} ${pr} `)) +
|
|
151
155
|
(error
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { css } from 'twin.macro'
|
|
3
3
|
import { memo } from 'react'
|
|
4
|
-
import ReactSelect, {
|
|
5
|
-
|
|
4
|
+
import ReactSelect, {
|
|
5
|
+
components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
|
|
6
|
+
DropdownIndicatorProps, MultiValueRemoveProps,
|
|
7
|
+
} from 'react-select'
|
|
6
8
|
import { ChevronUpDownIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
|
7
9
|
import { isFieldCached } from 'nitro-web'
|
|
8
10
|
import { getErrorFromState, deepFind, twMerge } from 'nitro-web/util'
|
|
@@ -19,11 +21,13 @@ type GetSelectStyle = {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** Select (all other props are passed to react-select) **/
|
|
22
|
-
type SelectProps = {
|
|
24
|
+
export type SelectProps = {
|
|
23
25
|
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
|
|
24
26
|
name: string
|
|
25
|
-
/** name used if not provided **/
|
|
26
|
-
|
|
27
|
+
/** inputId, the name is used if not provided **/
|
|
28
|
+
id?: string
|
|
29
|
+
/** 'container' id to pass to react-select **/
|
|
30
|
+
containerId?: string
|
|
27
31
|
/** The minimum width of the dropdown menu **/
|
|
28
32
|
minMenuWidth?: number
|
|
29
33
|
/** The prefix to add to the input **/
|
|
@@ -35,7 +39,7 @@ type SelectProps = {
|
|
|
35
39
|
/** The state object to get the value and check errors from **/
|
|
36
40
|
state?: { errors?: Errors, [key: string]: any } // was unknown|unknown[]
|
|
37
41
|
/** Select variations **/
|
|
38
|
-
|
|
42
|
+
mode?: 'country'|'customer'|''
|
|
39
43
|
/** Pass dependencies to break memoization, handy for onChange/onInputChange **/
|
|
40
44
|
deps?: unknown[]
|
|
41
45
|
/** All other props are passed to react-select **/
|
|
@@ -46,7 +50,7 @@ export const Select = memo(SelectBase, (prev, next) => {
|
|
|
46
50
|
return isFieldCached(prev, next)
|
|
47
51
|
})
|
|
48
52
|
|
|
49
|
-
function SelectBase({
|
|
53
|
+
function SelectBase({ id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', ...props }: SelectProps) {
|
|
50
54
|
let value: unknown|unknown[]
|
|
51
55
|
const error = getErrorFromState(state, name)
|
|
52
56
|
if (!name) throw new Error('Select component requires a `name` and `options` prop')
|
|
@@ -78,10 +82,11 @@ function SelectBase({ inputId, minMenuWidth, name, prefix='', onChange, options,
|
|
|
78
82
|
*/
|
|
79
83
|
{...props}
|
|
80
84
|
// @ts-expect-error
|
|
81
|
-
_nitro={{ prefix,
|
|
85
|
+
_nitro={{ prefix, mode }}
|
|
82
86
|
key={value as string}
|
|
83
87
|
unstyled={true}
|
|
84
|
-
inputId={
|
|
88
|
+
inputId={id || name}
|
|
89
|
+
id={containerId}
|
|
85
90
|
filterOption={(option, searchText) => {
|
|
86
91
|
if ((option.data as {fixed?: boolean}).fixed) return true
|
|
87
92
|
return filterFn(option, searchText)
|
|
@@ -161,7 +166,7 @@ function Control({ children, ...props }: ControlProps) {
|
|
|
161
166
|
// todo: check that the flag/prefix looks okay
|
|
162
167
|
const selectedOption = props.getValue()[0]
|
|
163
168
|
const optionFlag = (selectedOption as { flag?: string })?.flag
|
|
164
|
-
const _nitro = (props.selectProps as { _nitro?: { prefix?: string,
|
|
169
|
+
const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?: string } })?._nitro
|
|
165
170
|
return (
|
|
166
171
|
<components.Control {...props}>
|
|
167
172
|
{
|
|
@@ -173,7 +178,7 @@ function Control({ children, ...props }: ControlProps) {
|
|
|
173
178
|
{children}
|
|
174
179
|
</>
|
|
175
180
|
)
|
|
176
|
-
} else if (_nitro?.
|
|
181
|
+
} else if (_nitro?.mode == 'country') {
|
|
177
182
|
return (
|
|
178
183
|
<>
|
|
179
184
|
{ optionFlag && <Flag flag={optionFlag} /> }
|
|
@@ -201,10 +206,10 @@ function SingleValue(props: SingleValueProps) {
|
|
|
201
206
|
function Option(props: OptionProps) {
|
|
202
207
|
// todo: check that the flag looks okay
|
|
203
208
|
const data = props.data as { className?: string, flag?: string }
|
|
204
|
-
const _nitro = (props.selectProps as { _nitro?: {
|
|
209
|
+
const _nitro = (props.selectProps as { _nitro?: { mode?: string } })?._nitro
|
|
205
210
|
return (
|
|
206
211
|
<components.Option className={data.className} {...props}>
|
|
207
|
-
{ _nitro?.
|
|
212
|
+
{ _nitro?.mode == 'country' && <Flag flag={data.flag} /> }
|
|
208
213
|
<span class="flex-auto">{props.label}</span>
|
|
209
214
|
{props.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-1" />}
|
|
210
215
|
</components.Option>
|
|
@@ -248,7 +253,7 @@ const selectStyles = {
|
|
|
248
253
|
// Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
|
|
249
254
|
// Input container
|
|
250
255
|
control: {
|
|
251
|
-
base: 'rounded-md bg-white hover:cursor-pointer text-input-
|
|
256
|
+
base: 'rounded-md bg-white hover:cursor-pointer text-input-base outline outline-1 -outline-offset-1 '
|
|
252
257
|
+ '!min-h-0 outline-input-border',
|
|
253
258
|
focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
|
|
254
259
|
error: 'outline-danger',
|
|
@@ -273,8 +278,8 @@ const selectStyles = {
|
|
|
273
278
|
indicatorsContainer: 'p-1 px-2 gap-1',
|
|
274
279
|
indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
|
|
275
280
|
// Dropdown menu
|
|
276
|
-
menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-
|
|
277
|
-
groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-
|
|
281
|
+
menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-base overflow-hidden shadow-dropdown-ul',
|
|
282
|
+
groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-base',
|
|
278
283
|
noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
|
|
279
284
|
option: {
|
|
280
285
|
base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Filters, FiltersHandleType, FilterType,
|
|
4
4
|
} from 'nitro-web'
|
|
5
5
|
import { getCountryOptions, getCurrencyOptions, ucFirst } from 'nitro-web/util'
|
|
6
|
-
import { Check } from 'lucide-react'
|
|
6
|
+
import { Check, FileEditIcon } from 'lucide-react'
|
|
7
7
|
|
|
8
8
|
type StyleguideProps = {
|
|
9
9
|
className?: string
|
|
@@ -34,27 +34,40 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
34
34
|
})
|
|
35
35
|
const [filterState, setFilterState] = useState({})
|
|
36
36
|
const filtersRef = useRef<FiltersHandleType>(null)
|
|
37
|
-
const filters
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
const filters = useMemo(() => {
|
|
38
|
+
const filters: FilterType[] = [
|
|
39
|
+
{
|
|
40
|
+
type: 'date',
|
|
41
|
+
name: 'dateRange',
|
|
42
|
+
mode: 'range',
|
|
43
|
+
placeholder: 'Select a range...',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'search',
|
|
47
|
+
name: 'search',
|
|
48
|
+
label: 'Keyword Search',
|
|
49
|
+
placeholder: 'Job, employee name...',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'select',
|
|
53
|
+
name: 'status',
|
|
54
|
+
rowClassName: 'flex-1',
|
|
55
|
+
options: [
|
|
56
|
+
{ label: 'Pending', value: 'pending' },
|
|
57
|
+
{ label: 'Approved', value: 'approved' },
|
|
58
|
+
{ label: 'Rejected', value: 'rejected' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'color',
|
|
63
|
+
name: 'color',
|
|
64
|
+
label: 'Half column',
|
|
65
|
+
placeholder: 'Select color...',
|
|
66
|
+
rowClassName: 'flex-1',
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
return filters
|
|
70
|
+
}, [])
|
|
58
71
|
|
|
59
72
|
const options = useMemo(() => [
|
|
60
73
|
{ label: 'Open customer preview' },
|
|
@@ -182,6 +195,9 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
182
195
|
<div><Button IconRight="v">IconRight</Button></div>
|
|
183
196
|
<div><Button IconRightEnd="v" className="w-[190px]">IconRightEnd 190px</Button></div>
|
|
184
197
|
<div><Button color="primary" IconRight="v" isLoading>primary isLoading</Button></div>
|
|
198
|
+
<div><Button IconCenter={<FileEditIcon size={18}/>}></Button></div>
|
|
199
|
+
<div><Button size="sm" IconCenter={<FileEditIcon size={16}/>}></Button></div>
|
|
200
|
+
<div><Button size="xs" IconCenter={<FileEditIcon size={14}/>}></Button></div>
|
|
185
201
|
</div>
|
|
186
202
|
|
|
187
203
|
<h2 class="h3">Checkboxes</h2>
|
|
@@ -243,7 +259,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
243
259
|
<Select
|
|
244
260
|
// https://github.com/lipis/flag-icons
|
|
245
261
|
name="country"
|
|
246
|
-
|
|
262
|
+
mode="country"
|
|
247
263
|
state={state}
|
|
248
264
|
options={useMemo(() => getCountryOptions(injectedConfig.countries), [])}
|
|
249
265
|
onChange={(e) => onChange(setState, e)}
|
|
@@ -255,7 +271,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
255
271
|
// menuIsOpen={true}
|
|
256
272
|
placeholder="Select or add customer..."
|
|
257
273
|
name="customer"
|
|
258
|
-
|
|
274
|
+
mode="customer"
|
|
259
275
|
state={state}
|
|
260
276
|
onChange={onCustomerInputChange}
|
|
261
277
|
onInputChange={onCustomerSearch}
|
|
@@ -323,7 +339,7 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
323
339
|
</div>
|
|
324
340
|
<div>
|
|
325
341
|
<label for="brandColor">Brand Color</label>
|
|
326
|
-
<Field name="brandColor" type="color" state={state}
|
|
342
|
+
<Field name="brandColor" type="color" iconPos="left" state={state} onChange={(e) => onChange(setState, e)} />
|
|
327
343
|
</div>
|
|
328
344
|
<div>
|
|
329
345
|
<label for="amount">Amount ({state.amount})</label>
|
|
@@ -343,8 +359,8 @@ export function Styleguide({ className, elements, children }: StyleguideProps) {
|
|
|
343
359
|
<Field name="date-range" type="date" mode="range" prefix="Date:" state={state} onChange={(e) => onChange(setState, e)} />
|
|
344
360
|
</div>
|
|
345
361
|
<div>
|
|
346
|
-
<label for="date">Date (right aligned)</label>
|
|
347
|
-
<Field name="date" type="date" mode="
|
|
362
|
+
<label for="date">Date multi-select (right aligned)</label>
|
|
363
|
+
<Field name="date" type="date" mode="multiple" state={state} onChange={(e) => onChange(setState, e)} dir="bottom-right" />
|
|
348
364
|
</div>
|
|
349
365
|
</div>
|
|
350
366
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|
package/server/router.js
CHANGED
|
@@ -297,13 +297,19 @@ function resolveMiddleware (controllers, middleware, route, item) {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
const defaultMiddleware = {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
order: [
|
|
301
|
+
// Express middleware runtime order
|
|
302
|
+
'loadAssets',
|
|
303
|
+
'modifyRequest',
|
|
304
|
+
'parseUrlEncoded',
|
|
305
|
+
'parseJson',
|
|
306
|
+
'parseFile',
|
|
307
|
+
'beforeAPIRoute',
|
|
308
|
+
],
|
|
304
309
|
|
|
305
310
|
modifyRequest: (req, res, next) => {
|
|
306
311
|
// Handy boolean denoting that the request wants JSON returned
|
|
312
|
+
// global.start = new Date().getTime()
|
|
307
313
|
req.json = req.xhr || req.accepts(['html', 'json']) == 'json'
|
|
308
314
|
next()
|
|
309
315
|
},
|
|
@@ -334,13 +340,8 @@ const defaultMiddleware = {
|
|
|
334
340
|
})(req, res, () => { /*console.timeEnd('upload middleware'); */next() })
|
|
335
341
|
},
|
|
336
342
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
'parseUrlEncoded',
|
|
342
|
-
'parseJson',
|
|
343
|
-
'parseFile',
|
|
344
|
-
'beforeAPIRoute',
|
|
345
|
-
],
|
|
343
|
+
beforeAPIRoute: (req, res, next) => {
|
|
344
|
+
res.set('version', req.version)
|
|
345
|
+
next()
|
|
346
|
+
},
|
|
346
347
|
}
|
package/types/util.d.ts
CHANGED
|
@@ -503,6 +503,73 @@ export function onChange<T>(setState: React.Dispatch<React.SetStateAction<T>>, e
|
|
|
503
503
|
* @returns {string}
|
|
504
504
|
*/
|
|
505
505
|
export function pad(num?: number, padLeft?: number, fixedRight?: number): string;
|
|
506
|
+
/**
|
|
507
|
+
* Validates req.query "filters" against a config object, and returns a MongoDB-compatible query object.
|
|
508
|
+
* @param {{ [key: string]: string }} query - req.query
|
|
509
|
+
* E.g. {
|
|
510
|
+
* dateRange: '1749038400000,1749729600000',
|
|
511
|
+
* location: '10-RS',
|
|
512
|
+
* status: 'incomplete',
|
|
513
|
+
* search: 'John'
|
|
514
|
+
* }
|
|
515
|
+
* @param {{ [key: string]: 'string'|'number'|'search'|'dateRange'|string[] }} config - allowed filters and their rules
|
|
516
|
+
* E.g. {
|
|
517
|
+
* dateRange: 'dateRange',
|
|
518
|
+
* location: 'string',
|
|
519
|
+
* status: ['incomplete', 'complete'],
|
|
520
|
+
* search: 'string',
|
|
521
|
+
* }
|
|
522
|
+
* @example returned object (using the examples above):
|
|
523
|
+
* E.g. {
|
|
524
|
+
* date: { $gte: 1749038400000, $lte: 1749729600000 },
|
|
525
|
+
* location: '10-RS',
|
|
526
|
+
* status: 'incomplete',
|
|
527
|
+
* search: 'John'
|
|
528
|
+
* }
|
|
529
|
+
*/
|
|
530
|
+
export function parseFilters(query: {
|
|
531
|
+
[key: string]: string;
|
|
532
|
+
}, config: {
|
|
533
|
+
[key: string]: "string" | "number" | "search" | "dateRange" | string[];
|
|
534
|
+
}): {
|
|
535
|
+
[key: string]: string | number | string[] | {
|
|
536
|
+
$gte: number;
|
|
537
|
+
$lte?: number;
|
|
538
|
+
} | {
|
|
539
|
+
$search: string;
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
/**
|
|
543
|
+
* Parses req.query "pagination" and "sorting" fields and returns a monastery-compatible options object.
|
|
544
|
+
* @param {{ fieldsFlattened: object, name: string }} model - The Monastery model
|
|
545
|
+
* @param {{ page?: string, sort?: '1'|'-1', sortBy?: string }} query - req.query
|
|
546
|
+
* E.g. {
|
|
547
|
+
* page: '1',
|
|
548
|
+
* sort: '1',
|
|
549
|
+
* sortBy: 'createdAt'
|
|
550
|
+
* }
|
|
551
|
+
* @param {number} [limit=10]
|
|
552
|
+
* @example returned object (using the examples above):
|
|
553
|
+
* E.g. {
|
|
554
|
+
* limit: 10,
|
|
555
|
+
* skip: undefined,
|
|
556
|
+
* sort: { createdAt: 1 },
|
|
557
|
+
* }
|
|
558
|
+
*/
|
|
559
|
+
export function parseSortOptions(model: {
|
|
560
|
+
fieldsFlattened: object;
|
|
561
|
+
name: string;
|
|
562
|
+
}, query: {
|
|
563
|
+
page?: string;
|
|
564
|
+
sort?: "1" | "-1";
|
|
565
|
+
sortBy?: string;
|
|
566
|
+
}, limit?: number): {
|
|
567
|
+
limit: number;
|
|
568
|
+
skip: number;
|
|
569
|
+
sort: {
|
|
570
|
+
[x: string]: number;
|
|
571
|
+
};
|
|
572
|
+
};
|
|
506
573
|
/**
|
|
507
574
|
* Picks fields from an object
|
|
508
575
|
* @param {{ [key: string]: any }} obj
|
|
@@ -516,17 +583,16 @@ export function pick(obj: {
|
|
|
516
583
|
/**
|
|
517
584
|
*
|
|
518
585
|
* Parses a query string into an object, or returns the last known matching cache
|
|
519
|
-
* @param {string} searchString - location.search
|
|
586
|
+
* @param {string} searchString - location.search e.g. '?page=1&book=my+%2B+book'
|
|
520
587
|
* @param {boolean} [trueDefaults] - assign true to empty values
|
|
521
588
|
* @returns {{[key: string]: string|true}} - e.g. { page: '1' }
|
|
522
|
-
* UPDATE: removed array values, e.g. '?page=1&page=2' will return { page: '2' }
|
|
523
589
|
*/
|
|
524
590
|
export function queryObject(searchString: string, trueDefaults?: boolean): {
|
|
525
591
|
[key: string]: string | true;
|
|
526
592
|
};
|
|
527
593
|
/**
|
|
528
594
|
* Parses a query string into an array of objects
|
|
529
|
-
* @param {string} searchString - location.search
|
|
595
|
+
* @param {string} searchString - location.search, e.g. '?page=1'
|
|
530
596
|
* @returns {object[]} - e.g. [{ page: '1' }]
|
|
531
597
|
*/
|
|
532
598
|
export function queryArray(searchString: string): object[];
|
package/types/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../util.js"],"names":[],"mappings":"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"}
|
package/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
1
3
|
type InjectedConfig = {
|
|
2
4
|
awsUrl?: string
|
|
3
5
|
clientUrl: string
|
|
@@ -19,7 +21,7 @@ export type Config = InjectedConfig & {
|
|
|
19
21
|
// Non-injectable config on the client
|
|
20
22
|
beforeApp?: () => Promise<object>
|
|
21
23
|
beforeStoreUpdate?: (prevStore: Store | null, newData: Store) => Store
|
|
22
|
-
middleware?:
|
|
24
|
+
middleware?: {[key: string]: (route: any, store: any) => undefined | { redirect: string }}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type User = {
|
package/util.js
CHANGED
|
@@ -1199,6 +1199,112 @@ export function pad (num=0, padLeft=0, fixedRight) {
|
|
|
1199
1199
|
}
|
|
1200
1200
|
}
|
|
1201
1201
|
|
|
1202
|
+
/**
|
|
1203
|
+
* Validates req.query "filters" against a config object, and returns a MongoDB-compatible query object.
|
|
1204
|
+
* @param {{ [key: string]: string }} query - req.query
|
|
1205
|
+
* E.g. {
|
|
1206
|
+
* dateRange: '1749038400000,1749729600000',
|
|
1207
|
+
* location: '10-RS',
|
|
1208
|
+
* status: 'incomplete',
|
|
1209
|
+
* search: 'John'
|
|
1210
|
+
* }
|
|
1211
|
+
* @param {{ [key: string]: 'string'|'number'|'search'|'dateRange'|string[] }} config - allowed filters and their rules
|
|
1212
|
+
* E.g. {
|
|
1213
|
+
* dateRange: 'dateRange',
|
|
1214
|
+
* location: 'string',
|
|
1215
|
+
* status: ['incomplete', 'complete'],
|
|
1216
|
+
* search: 'string',
|
|
1217
|
+
* }
|
|
1218
|
+
* @example returned object (using the examples above):
|
|
1219
|
+
* E.g. {
|
|
1220
|
+
* date: { $gte: 1749038400000, $lte: 1749729600000 },
|
|
1221
|
+
* location: '10-RS',
|
|
1222
|
+
* status: 'incomplete',
|
|
1223
|
+
* search: 'John'
|
|
1224
|
+
* }
|
|
1225
|
+
*/
|
|
1226
|
+
export function parseFilters(query, config) {
|
|
1227
|
+
/** @type {{ [key: string]: string|number|{ $gte: number; $lte?: number; }|{ $search: string }|string[] }} */
|
|
1228
|
+
const mongoQuery = {}
|
|
1229
|
+
|
|
1230
|
+
for (const key in query) {
|
|
1231
|
+
const val = query[key]
|
|
1232
|
+
const rule = config[key]
|
|
1233
|
+
|
|
1234
|
+
if (!rule) {
|
|
1235
|
+
continue
|
|
1236
|
+
|
|
1237
|
+
} else if (rule === 'string') {
|
|
1238
|
+
if (typeof val !== 'string') throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a string.`)
|
|
1239
|
+
mongoQuery[key] = val
|
|
1240
|
+
|
|
1241
|
+
} else if (rule === 'number') {
|
|
1242
|
+
if (typeof val !== 'number' || isNaN(val)) throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a number.`)
|
|
1243
|
+
mongoQuery[key] = parseFloat(val)
|
|
1244
|
+
|
|
1245
|
+
} else if (rule === 'search') {
|
|
1246
|
+
if (typeof val !== 'string') throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a string.`)
|
|
1247
|
+
mongoQuery['$text'] = { $search: val }
|
|
1248
|
+
|
|
1249
|
+
} else if (Array.isArray(rule)) {
|
|
1250
|
+
if (!rule.includes(val)) {
|
|
1251
|
+
throw new Error(`The "${key}" filter has an invalid value "${val}". Allowed values: "${rule.join('", "')}"`)
|
|
1252
|
+
}
|
|
1253
|
+
mongoQuery[key] = val
|
|
1254
|
+
|
|
1255
|
+
} else if (rule === 'dateRange') {
|
|
1256
|
+
const [start, end] = val.split(',').map(Number)
|
|
1257
|
+
if (isNaN(start) && isNaN(end)) throw new Error(`The "${key}" filter has an invalid value "${val}". Expected a date range.`)
|
|
1258
|
+
else if (isNaN(start)) mongoQuery.date = { $gte: 0, $lte: end }
|
|
1259
|
+
else if (isNaN(end)) mongoQuery.date = { $gte: start }
|
|
1260
|
+
else mongoQuery.date = { $gte: start, $lte: end }
|
|
1261
|
+
|
|
1262
|
+
} else {
|
|
1263
|
+
throw new Error(`Unknown filter type "${rule}" in the config.`)
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return mongoQuery
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Parses req.query "pagination" and "sorting" fields and returns a monastery-compatible options object.
|
|
1272
|
+
* @param {{ fieldsFlattened: object, name: string }} model - The Monastery model
|
|
1273
|
+
* @param {{ page?: string, sort?: '1'|'-1', sortBy?: string }} query - req.query
|
|
1274
|
+
* E.g. {
|
|
1275
|
+
* page: '1',
|
|
1276
|
+
* sort: '1',
|
|
1277
|
+
* sortBy: 'createdAt'
|
|
1278
|
+
* }
|
|
1279
|
+
* @param {number} [limit=10]
|
|
1280
|
+
* @example returned object (using the examples above):
|
|
1281
|
+
* E.g. {
|
|
1282
|
+
* limit: 10,
|
|
1283
|
+
* skip: undefined,
|
|
1284
|
+
* sort: { createdAt: 1 },
|
|
1285
|
+
* }
|
|
1286
|
+
*/
|
|
1287
|
+
export function parseSortOptions(model, query, limit = 10) {
|
|
1288
|
+
const page = parseInt(query.page || '') || 1
|
|
1289
|
+
|
|
1290
|
+
// Validate sortBy value
|
|
1291
|
+
const sortBy = query.sortBy || 'createdAt'
|
|
1292
|
+
const fields = Object.keys(model.fieldsFlattened)
|
|
1293
|
+
if (!fields.includes(sortBy)) {
|
|
1294
|
+
throw new Error(`"${sortBy}" is an invalid sortBy value for the "${model.name}" model.`)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const sort = sortBy === 'createdAt' && !query.sort ? -1 : (parseInt(query.sort || '') || 1)
|
|
1298
|
+
|
|
1299
|
+
return {
|
|
1300
|
+
limit: limit + 1, // get an extra row to signal there are more pages
|
|
1301
|
+
skip: page > 1 ? (page - 1) * limit : undefined,
|
|
1302
|
+
sort: {
|
|
1303
|
+
[sortBy]: sort,
|
|
1304
|
+
},
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1202
1308
|
/**
|
|
1203
1309
|
* Picks fields from an object
|
|
1204
1310
|
* @param {{ [key: string]: any }} obj
|
|
@@ -1224,44 +1330,34 @@ export function pick (obj, keys) {
|
|
|
1224
1330
|
/**
|
|
1225
1331
|
*
|
|
1226
1332
|
* Parses a query string into an object, or returns the last known matching cache
|
|
1227
|
-
* @param {string} searchString - location.search
|
|
1333
|
+
* @param {string} searchString - location.search e.g. '?page=1&book=my+%2B+book'
|
|
1228
1334
|
* @param {boolean} [trueDefaults] - assign true to empty values
|
|
1229
1335
|
* @returns {{[key: string]: string|true}} - e.g. { page: '1' }
|
|
1230
|
-
* UPDATE: removed array values, e.g. '?page=1&page=2' will return { page: '2' }
|
|
1231
1336
|
*/
|
|
1232
1337
|
export function queryObject (searchString, trueDefaults) {
|
|
1233
|
-
|
|
1338
|
+
if (searchString.startsWith('?')) searchString = searchString.slice(1)
|
|
1234
1339
|
const uniqueKey = searchString + (trueDefaults ? '-true' : '')
|
|
1235
|
-
/** @type {{[key: string]: string|true}} */
|
|
1236
|
-
let obj = {}
|
|
1237
1340
|
|
|
1238
1341
|
if (searchString === '') return {}
|
|
1239
1342
|
if (!queryObjectCache) queryObjectCache = {}
|
|
1240
1343
|
if (queryObjectCache[uniqueKey]) return queryObjectCache[uniqueKey]
|
|
1241
1344
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const key = partArr[0]
|
|
1249
|
-
const isEmpty = !partArr[1] && partArr[1] != '0'
|
|
1250
|
-
|
|
1251
|
-
if (trueDefaults === true) {
|
|
1252
|
-
obj[key] = isEmpty ? true : decodeURIComponent(partArr[1])
|
|
1253
|
-
} else {
|
|
1254
|
-
obj[key] = decodeURIComponent(partArr[1])
|
|
1345
|
+
const params = new URLSearchParams(searchString)
|
|
1346
|
+
/** @type {{[key: string]: string|true}} */
|
|
1347
|
+
const result = Object.fromEntries(params.entries())
|
|
1348
|
+
if (trueDefaults) {
|
|
1349
|
+
for (const key in result) {
|
|
1350
|
+
if (!result[key] && result[key] !== '0') result[key] = true
|
|
1255
1351
|
}
|
|
1256
|
-
}
|
|
1352
|
+
}
|
|
1257
1353
|
|
|
1258
|
-
queryObjectCache[uniqueKey] =
|
|
1259
|
-
return
|
|
1354
|
+
queryObjectCache[uniqueKey] = result
|
|
1355
|
+
return result
|
|
1260
1356
|
}
|
|
1261
1357
|
|
|
1262
1358
|
/**
|
|
1263
1359
|
* Parses a query string into an array of objects
|
|
1264
|
-
* @param {string} searchString - location.search
|
|
1360
|
+
* @param {string} searchString - location.search, e.g. '?page=1'
|
|
1265
1361
|
* @returns {object[]} - e.g. [{ page: '1' }]
|
|
1266
1362
|
*/
|
|
1267
1363
|
export function queryArray (searchString) {
|
|
@@ -1341,7 +1437,7 @@ export async function request (route, data, event, isLoading) {
|
|
|
1341
1437
|
|
|
1342
1438
|
const [res] = await Promise.allSettled([
|
|
1343
1439
|
axiosPromise,
|
|
1344
|
-
setTimeoutPromise(() => {}, 200), // eslint-disable-line
|
|
1440
|
+
// setTimeoutPromise(() => {}, 200), // eslint-disable-line
|
|
1345
1441
|
])
|
|
1346
1442
|
|
|
1347
1443
|
// success
|
|
@@ -1580,7 +1676,13 @@ export function trim (string) {
|
|
|
1580
1676
|
*/
|
|
1581
1677
|
export function twMerge(...args) {
|
|
1582
1678
|
const ignoredClasses = /** @type {string[]} */([])
|
|
1583
|
-
const ignoreClasses = [
|
|
1679
|
+
const ignoreClasses = [
|
|
1680
|
+
'text-button-xs',
|
|
1681
|
+
'text-button-sm',
|
|
1682
|
+
'text-button-md',
|
|
1683
|
+
'text-button-lg',
|
|
1684
|
+
'text-input-base',
|
|
1685
|
+
]
|
|
1584
1686
|
const classes = args.filter(Boolean).join(' ').split(' ')
|
|
1585
1687
|
|
|
1586
1688
|
const filteredClasses = classes.filter(c => {
|