nitro-web 0.0.65 → 0.0.67
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/dropdown.tsx +12 -6
- package/components/partials/element/filters.tsx +50 -48
- 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 +48 -23
- 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/types/util.d.ts +67 -0
- package/types/util.d.ts.map +1 -1
- package/util.js +113 -1
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,9 +1,10 @@
|
|
|
1
1
|
import { css } from 'twin.macro'
|
|
2
2
|
import { forwardRef, cloneElement } from 'react'
|
|
3
|
-
import { getSelectStyle } from 'nitro-web'
|
|
3
|
+
import { getSelectStyle, twMerge } from 'nitro-web'
|
|
4
4
|
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
|
5
5
|
|
|
6
6
|
type DropdownProps = {
|
|
7
|
+
allowOverflow?: boolean
|
|
7
8
|
animate?: boolean
|
|
8
9
|
children?: React.ReactNode
|
|
9
10
|
className?: string
|
|
@@ -18,20 +19,23 @@ type DropdownProps = {
|
|
|
18
19
|
/** The content to render inside the top of the dropdown **/
|
|
19
20
|
menuContent?: React.ReactNode
|
|
20
21
|
menuClassName?: string
|
|
22
|
+
menuOptionClassName?: string
|
|
21
23
|
menuIsOpen?: boolean
|
|
22
24
|
menuToggles?: boolean
|
|
23
25
|
toggleCallback?: (isActive: boolean) => void
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const Dropdown = forwardRef(function Dropdown({
|
|
29
|
+
allowOverflow=false,
|
|
27
30
|
animate=true,
|
|
28
31
|
children,
|
|
29
32
|
className,
|
|
30
33
|
dir,
|
|
31
34
|
options,
|
|
32
35
|
isHoverable,
|
|
33
|
-
minWidth,
|
|
36
|
+
minWidth, // remove in favour of menuClassName
|
|
34
37
|
menuClassName,
|
|
38
|
+
menuOptionClassName,
|
|
35
39
|
menuContent,
|
|
36
40
|
menuIsOpen,
|
|
37
41
|
menuToggles=true,
|
|
@@ -92,6 +96,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
92
96
|
(isHoverable ? ' is-hoverable' : '') +
|
|
93
97
|
(isActive ? ' is-active' : '') +
|
|
94
98
|
(!animate ? ' no-animation' : '') +
|
|
99
|
+
(allowOverflow ? ' is-allowOverflow' : '') +
|
|
95
100
|
' nitro-dropdown' +
|
|
96
101
|
(className ? ` ${className}` : '')
|
|
97
102
|
}
|
|
@@ -108,7 +113,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
108
113
|
}
|
|
109
114
|
<ul
|
|
110
115
|
style={{ minWidth }}
|
|
111
|
-
class={`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName}`}
|
|
116
|
+
class={twMerge(`${menuStyle} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName}`)}
|
|
112
117
|
>
|
|
113
118
|
{menuContent}
|
|
114
119
|
{
|
|
@@ -117,7 +122,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
117
122
|
return (
|
|
118
123
|
<li
|
|
119
124
|
key={i}
|
|
120
|
-
className={`${optionStyle} ${option.className}`}
|
|
125
|
+
className={twMerge(`${optionStyle} ${option.className} ${menuOptionClassName}`)}
|
|
121
126
|
onClick={(e: React.MouseEvent) => onClick(option, e)}
|
|
122
127
|
>
|
|
123
128
|
<span class="flex-auto">{option.label}</span>
|
|
@@ -137,8 +142,7 @@ const style = css`
|
|
|
137
142
|
transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease, max-width 0s 0.15s ease, max-height 0s 0.15s ease;
|
|
138
143
|
max-width: 0; // handy if the dropdown ul exceeds the viewport width
|
|
139
144
|
max-height: 0; // handy if the dropdown ul exceeds the viewport height
|
|
140
|
-
|
|
141
|
-
pointer-events: none;
|
|
145
|
+
pointer-events: none;
|
|
142
146
|
}
|
|
143
147
|
&.is-bottom-right,
|
|
144
148
|
&.is-top-right {
|
|
@@ -175,6 +179,8 @@ const style = css`
|
|
|
175
179
|
max-width: 1000px;
|
|
176
180
|
max-height: 1000px;
|
|
177
181
|
pointer-events: auto;
|
|
182
|
+
}
|
|
183
|
+
&.is-allowOverflow > ul {
|
|
178
184
|
overflow: visible;
|
|
179
185
|
}
|
|
180
186
|
&.is-bottom-left > ul,
|
|
@@ -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,6 +32,8 @@ type FiltersProps = {
|
|
|
31
32
|
buttonClassName?: string
|
|
32
33
|
buttonText?: string
|
|
33
34
|
buttonCounterClassName?: string
|
|
35
|
+
filtersContainerClassName?: string
|
|
36
|
+
menuClassName?: string
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export type FiltersHandleType = {
|
|
@@ -40,14 +43,27 @@ export type FiltersHandleType = {
|
|
|
40
43
|
const debounceTime = 250
|
|
41
44
|
|
|
42
45
|
export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
43
|
-
filters,
|
|
46
|
+
filters,
|
|
47
|
+
setState: setState2,
|
|
48
|
+
state: state2,
|
|
49
|
+
buttonClassName,
|
|
50
|
+
buttonCounterClassName,
|
|
51
|
+
buttonProps,
|
|
52
|
+
buttonText,
|
|
53
|
+
dropdownProps,
|
|
54
|
+
elements,
|
|
55
|
+
filtersContainerClassName,
|
|
56
|
+
menuClassName,
|
|
44
57
|
}, ref) => {
|
|
45
58
|
const location = useLocation()
|
|
46
59
|
const navigate = useNavigate()
|
|
47
|
-
const stateRef = useRef(state)
|
|
48
60
|
const [lastUpdated, setLastUpdated] = useState(0)
|
|
49
61
|
const [debouncedSubmit] = useState(() => debounce(submit, debounceTime))
|
|
50
|
-
const
|
|
62
|
+
const [state3, setState3] = useState(() => ({ ...queryObject(location.search) }))
|
|
63
|
+
const [state, setState] = [state2 || state3, setState2 || setState3]
|
|
64
|
+
const stateRef = useRef(state)
|
|
65
|
+
const count = useMemo(() => Object.keys(state).filter((k) => state[k] && filters?.some((f) => f.name === k)).length, [state, filters])
|
|
66
|
+
|
|
51
67
|
const Elements = {
|
|
52
68
|
Button: elements?.Button || Button,
|
|
53
69
|
Dropdown: elements?.Dropdown || Dropdown,
|
|
@@ -85,9 +101,7 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
|
85
101
|
|
|
86
102
|
function resetAll(e: React.MouseEvent<HTMLButtonElement>) {
|
|
87
103
|
e.preventDefault()
|
|
88
|
-
setState((s) => (
|
|
89
|
-
...(s.page ? { page: s.page } : {}), // keep pagination
|
|
90
|
-
} as FilterState))
|
|
104
|
+
setState((s) => omit(s, filters?.map((f) => f.name) || []) as FilterState)
|
|
91
105
|
onAfterChange()
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -106,60 +120,48 @@ export const Filters = forwardRef<FiltersHandleType, FiltersProps>(({
|
|
|
106
120
|
const queryStr = queryString(omit(stateRef.current, includePagination ? [] : ['page']))
|
|
107
121
|
navigate(location.pathname + queryStr, { replace: true })
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
if (!filters) return null
|
|
123
|
+
|
|
111
124
|
return (
|
|
112
125
|
<Elements.Dropdown
|
|
113
126
|
dir="bottom-right"
|
|
127
|
+
allowOverflow={true}
|
|
114
128
|
// menuIsOpen={true}
|
|
115
|
-
menuClassName=
|
|
129
|
+
menuClassName={twMerge(`!rounded-lg min-w-[330px] ${menuClassName || ''}`)}
|
|
116
130
|
menuContent={
|
|
117
|
-
<div
|
|
131
|
+
<div>
|
|
118
132
|
<div class="flex justify-between items-center border-b p-4 py-3.5">
|
|
119
133
|
<div class="text-lg font-semibold">Filters</div>
|
|
120
134
|
<Button color="clear" size="sm" onClick={resetAll}>Reset All</Button>
|
|
121
135
|
</div>
|
|
122
|
-
<div class="
|
|
136
|
+
{/* <div class="w-[1330px] bg-red-500 absolute">
|
|
137
|
+
This div shouldnt produce a page scrollbar when the dropdown is closed.
|
|
138
|
+
But should be visibile if allowedOverflow is true.
|
|
139
|
+
</div> */}
|
|
140
|
+
<div class={twMerge(`flex flex-wrap gap-4 px-4 py-4 pb-6 ${filtersContainerClassName || ''}`)}>
|
|
123
141
|
{
|
|
124
|
-
filters
|
|
125
|
-
<div key={
|
|
142
|
+
filters?.map(({label, rowClassName, ...filter}, i) => (
|
|
143
|
+
<div key={i} class={twMerge(`w-full ${rowClassName||''}`)}>
|
|
126
144
|
<div class="flex justify-between">
|
|
127
|
-
<label for={filter.name}>{
|
|
145
|
+
<label for={filter.id || filter.name}>{label || camelCaseToTitle(filter.name)}</label>
|
|
128
146
|
<a href="#" class="label font-normal text-secondary underline" onClick={(e) => reset(e, filter)}>Reset</a>
|
|
129
147
|
</div>
|
|
130
148
|
{
|
|
131
|
-
|
|
132
|
-
<Elements.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
type={filter.type}
|
|
136
|
-
placeholder={filter.placeholder}
|
|
137
|
-
state={state}
|
|
138
|
-
onChange={onInputChange}
|
|
139
|
-
/>
|
|
140
|
-
}
|
|
141
|
-
{
|
|
142
|
-
filter.type === 'date' &&
|
|
143
|
-
<Elements.Field
|
|
144
|
-
class="mb-4"
|
|
145
|
-
name={filter.name}
|
|
146
|
-
type="date"
|
|
147
|
-
mode="range"
|
|
149
|
+
filter.type === 'select' &&
|
|
150
|
+
<Elements.Select
|
|
151
|
+
{...filter}
|
|
152
|
+
class="mb-0"
|
|
148
153
|
state={state}
|
|
149
154
|
onChange={onInputChange}
|
|
150
|
-
|
|
155
|
+
type={undefined}
|
|
151
156
|
/>
|
|
152
157
|
}
|
|
153
158
|
{
|
|
154
|
-
filter.type
|
|
155
|
-
<Elements.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
type="country"
|
|
159
|
+
filter.type !== 'select' &&
|
|
160
|
+
<Elements.Field
|
|
161
|
+
{...filter}
|
|
162
|
+
class="mb-0"
|
|
159
163
|
state={state}
|
|
160
|
-
options={filter.enums || []}
|
|
161
164
|
onChange={onInputChange}
|
|
162
|
-
placeholder={filter.placeholder}
|
|
163
165
|
/>
|
|
164
166
|
}
|
|
165
167
|
</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,11 +61,19 @@ 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
|
-
const
|
|
53
|
-
|
|
72
|
+
const arrOfNumbers = typeof value === 'string'
|
|
73
|
+
? value.split(/\s*,\s*/g).map(o => parseFloat(o))
|
|
74
|
+
: Array.isArray(value) ? value : [value]
|
|
75
|
+
const out = arrOfNumbers.map(date => isValid(date) ? new Date(date as number) : null) /// changed to null
|
|
76
|
+
return out
|
|
54
77
|
}, [value])
|
|
55
78
|
|
|
56
79
|
// Hold the input value in state
|
|
@@ -59,7 +82,7 @@ export function FieldDate({
|
|
|
59
82
|
// Update the date's inputValue (text) when the value changes outside of the component
|
|
60
83
|
useEffect(() => {
|
|
61
84
|
if (new Date().getTime() > lastUpdated + 100) setInputValue(getInputValue(dates))
|
|
62
|
-
}, [
|
|
85
|
+
}, [dates])
|
|
63
86
|
|
|
64
87
|
// Get the prefix content width
|
|
65
88
|
useEffect(() => {
|
|
@@ -70,15 +93,8 @@ export function FieldDate({
|
|
|
70
93
|
if (mode == 'single' && !showTime) dropdownRef.current?.setIsActive(false) // Close the dropdown
|
|
71
94
|
setInputValue(getInputValue(value))
|
|
72
95
|
// Update the value
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
setLastUpdated(new Date().getTime())
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
|
|
80
|
-
const _dates = Array.isArray(dates) ? dates : [dates]
|
|
81
|
-
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
96
|
+
onChange({ target: { name: props.name, value: getOutputValue(value) } })
|
|
97
|
+
setLastUpdated(new Date().getTime())
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
@@ -104,12 +120,20 @@ export function FieldDate({
|
|
|
104
120
|
|
|
105
121
|
// Update the value
|
|
106
122
|
const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
setLastUpdated(new Date().getTime())
|
|
110
|
-
}
|
|
123
|
+
onChange({ target: { name: props.name, value: getOutputValue(value) }})
|
|
124
|
+
setLastUpdated(new Date().getTime())
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
function getInputValue(value: Date|number|null|(Date|number|null)[]) {
|
|
128
|
+
const _dates = Array.isArray(value) ? value : [value]
|
|
129
|
+
return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getOutputValue(value: Date|number|null|(Date|number|null)[]): any {
|
|
133
|
+
// console.log(value)
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
return (
|
|
114
138
|
<Dropdown
|
|
115
139
|
ref={dropdownRef}
|
|
@@ -120,7 +144,8 @@ export function FieldDate({
|
|
|
120
144
|
menuContent={
|
|
121
145
|
<div className="flex">
|
|
122
146
|
<Calendar
|
|
123
|
-
|
|
147
|
+
// Calendar actually accepts an array of dates, but the type is not typed correctly
|
|
148
|
+
{...{ mode: mode, value: dates as any, numberOfMonths: numberOfMonths, month: month }}
|
|
124
149
|
preserveTime={!!showTime}
|
|
125
150
|
onChange={onCalendarChange}
|
|
126
151
|
className="pt-1 pb-2 px-3"
|
|
@@ -135,7 +160,7 @@ export function FieldDate({
|
|
|
135
160
|
{
|
|
136
161
|
prefix &&
|
|
137
162
|
// 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-
|
|
163
|
+
<span className="z-[0] col-start-1 row-start-1 self-center select-none justify-self-start text-input-base ml-3">
|
|
139
164
|
{prefix}
|
|
140
165
|
</span>
|
|
141
166
|
}
|
|
@@ -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.67",
|
|
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/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
|
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/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[key] = { $gte: 0, $lte: end }
|
|
1259
|
+
else if (isNaN(end)) mongoQuery[key] = { $gte: start }
|
|
1260
|
+
else mongoQuery[key] = { $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
|
|
@@ -1570,7 +1676,13 @@ export function trim (string) {
|
|
|
1570
1676
|
*/
|
|
1571
1677
|
export function twMerge(...args) {
|
|
1572
1678
|
const ignoredClasses = /** @type {string[]} */([])
|
|
1573
|
-
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
|
+
]
|
|
1574
1686
|
const classes = args.filter(Boolean).join(' ').split(' ')
|
|
1575
1687
|
|
|
1576
1688
|
const filteredClasses = classes.filter(c => {
|