nitro-web 0.2.3 → 0.2.5

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.
@@ -142,7 +142,7 @@ export const Dropdown = forwardRef(function Dropdown({
142
142
 
143
143
  function onMouseDown(e: { key: string, preventDefault: Function, target: EventTarget | null }) {
144
144
  if (e.key && e.key != 'Enter') return
145
- if (e.key) e.preventDefault() // for button, stops buttons firing twice
145
+ if (e.key) e.preventDefault() // Enter on a <button> also fires a native click, cancel to avoid toggling twice
146
146
 
147
147
  if (!isHoverable && !menuIsOpen && ((menuToggles || e.key) || !isActive)) {
148
148
  if (isActive && preventCloseOnClickChild && dropdownRef.current?.contains(e.target as Node)) return // keep active
@@ -150,13 +150,20 @@ export const Dropdown = forwardRef(function Dropdown({
150
150
  }
151
151
  }
152
152
 
153
+ function onClick(e: React.MouseEvent) {
154
+ e.preventDefault()
155
+ e.stopPropagation()
156
+ }
157
+
153
158
  function onItemClick(option: { onClick?: Function, preventCloseOnClick?: boolean }, e: React.MouseEvent) {
154
159
  if (option.onClick) option.onClick(e, option)
155
160
  if (!menuIsOpen && !option?.preventCloseOnClick) setIsActive(!isActive)
156
161
  }
157
162
 
158
163
  return (
159
- <div
164
+ <div
165
+ ref={dropdownRef}
166
+ css={style}
160
167
  class={
161
168
  `relative is-${direction || dir}` + // until hovered, show the original direction to prevent scrollbars
162
169
  (isHoverable ? ' is-hoverable' : '') +
@@ -166,28 +173,36 @@ export const Dropdown = forwardRef(function Dropdown({
166
173
  ' nitro-dropdown' +
167
174
  (className ? ` ${className}` : '')
168
175
  }
169
- // When the dropdown is inside a link, prevent click propagation
170
- onClick={(e) => { e.preventDefault() }}
171
- // When the dropdown is inside a link, prevent auto <a> focus propagation (for the window listener)
172
- onMouseDown={(e) => { e.preventDefault() }}
173
- ref={dropdownRef}
174
- css={style}
175
176
  >
176
177
  {
177
178
  (Array.isArray(children) ? children : [children]).map((el, key) => {
178
179
  const onKeyDown = onMouseDown
179
180
  if (!el.type) throw new Error('Dropdown component requires a valid child element')
180
- return cloneElement(el, { key, onMouseDown, onKeyDown }) // adds onClick
181
+ return cloneElement(el, { key, onMouseDown, onKeyDown, onClick })
181
182
  })
182
183
  }
183
184
  <ul
184
- style={{
185
- minWidth: minWidth,
186
- maxHeight: maxHeight,
185
+ // Mousedown on non-focusable menu content now focuses the ul instead of escaping to parent link row,
186
+ // so the window 'focus' listener doesn't close the menu early
187
+ tabIndex={-1}
188
+ // Keep mousedown from bubbling to parent row handlers (focus containment is handled by tabIndex above)
189
+ onMouseDown={(e: React.MouseEvent) => e.stopPropagation()}
190
+ onClick={(e: React.MouseEvent) => {
191
+ // Keep menu clicks from reaching parent row click handlers
192
+ e.stopPropagation()
193
+ // Cancel parent <a> row navigation, unless a link INSIDE the menu was clicked
194
+ // (closest() doesn't stop at the ul, so check the matched link is contained within it)
195
+ const link = (e.target as HTMLElement).closest('a[href]')
196
+ if (!link || !e.currentTarget.contains(link)) e.preventDefault()
197
+ }}
198
+ style={{
199
+ minWidth: minWidth,
200
+ maxHeight: maxHeight,
187
201
  ...(maxHeight ? { overflow: 'auto' } : {}),
188
202
  }}
189
- class={
190
- twMerge(`${menuStyle} ${ready ? 'is-ready' : ''} absolute invisible opacity-0 select-none min-w-full z-[1] ${menuClassName||''}`)}
203
+ class={twMerge(
204
+ `${menuStyle} ${ready ? 'is-ready' : ''} absolute invisible opacity-0 select-none outline-none min-w-full z-[1] ${menuClassName||''}`
205
+ )}
191
206
  >
192
207
  {menuContent}
193
208
  {
@@ -26,8 +26,10 @@ export type TableRow = {
26
26
  export type TableProps<T> = {
27
27
  // todo: put the classnames into a single object
28
28
  columns: TableColumn[]
29
- rows: T[]
30
29
  generateTd: (col: TableColumn, row: T, i: number, isLast: boolean) => JSX.Element | null
30
+ rows: T[]
31
+ // Optional settings
32
+ addOverflowScroll?: boolean
31
33
  generateCheckboxActions?: (selectedRowIds: string[], setSelectedRowIds: (selectedRowIds: string[]) => void) => JSX.Element | null
32
34
  headerHeightMin?: number
33
35
  rowHeightMin?: number
@@ -39,7 +41,10 @@ export type TableProps<T> = {
39
41
  rowLink?: (row: T) => string
40
42
  columnGap?: number
41
43
  columnPaddingX?: number
44
+ checkboxSize?: number
45
+ // Class names
42
46
  className?: string
47
+ scrollContainerClassName?: string
43
48
  tableClassName?: string
44
49
  rowClassName?: string
45
50
  rowClassNameFn?: (row: T, i: number) => string
@@ -48,8 +53,8 @@ export type TableProps<T> = {
48
53
  columnSelectedClassName?: string
49
54
  columnHeaderClassName?: string
50
55
  checkboxClassName?: string
51
- checkboxSize?: number
52
56
  loadingOverlayClassName?: string
57
+ // Loading
53
58
  isLoading?:boolean
54
59
  loadingMessage?: string
55
60
  showLoadingInline?: boolean|JSX.Element
@@ -57,22 +62,26 @@ export type TableProps<T> = {
57
62
  }
58
63
 
59
64
  export function Table<T extends TableRow>({
60
- rows,
61
65
  columns: columnsProp,
62
- generateTd,
66
+ generateTd,
67
+ rows,
68
+ // Optional settings
69
+ addOverflowScroll,
70
+ checkboxSize=16,
71
+ columnGap=11,
72
+ columnPaddingX=11,
63
73
  generateCheckboxActions,
64
74
  headerHeightMin=40,
65
- rowHeightMin=48,
66
75
  rowContentHeightMax,
67
- rowLinesMax,
68
- rowSideColor,
69
76
  rowGap=0,
70
- rowOnClick,
77
+ rowHeightMin=48,
78
+ rowLinesMax,
71
79
  rowLink,
72
- columnGap=11,
73
- columnPaddingX=11,
80
+ rowOnClick,
81
+ rowSideColor,
74
82
  // Class names
75
83
  className,
84
+ scrollContainerClassName,
76
85
  tableClassName,
77
86
  rowClassName,
78
87
  rowClassNameFn,
@@ -81,7 +90,6 @@ export function Table<T extends TableRow>({
81
90
  columnSelectedClassName,
82
91
  columnHeaderClassName,
83
92
  checkboxClassName,
84
- checkboxSize=16,
85
93
  loadingOverlayClassName,
86
94
  // Loading
87
95
  isLoading=false,
@@ -161,190 +169,192 @@ export function Table<T extends TableRow>({
161
169
  }, [rowSideColor, columnPaddingX, columnGap])
162
170
 
163
171
  return (
164
- <div
165
- style={{ marginTop: -rowGap }}
166
- className={twMerge('overflow-x-auto thin-scrollbar', className)}
167
- >
172
+ <div className={className}>
168
173
  <div
169
- style={{ borderSpacing: `0 ${rowGap}px` }}
170
- className={twMerge('table w-full border-separate', showLoadingOverlay ? 'relative' : '', tableClassName)}
174
+ style={{ marginTop: -rowGap, marginBottom: -rowGap }}
175
+ className={twMerge(addOverflowScroll ? 'overflow-x-auto thin-scrollbar' : '', scrollContainerClassName)}
171
176
  >
172
- {/* Thead row */}
173
- <div className="table-row relative">
174
- {
175
- columns.map((col, j) => {
176
- const disableSort = col.disableSort || selectedRowIds.length
177
- const { pl, pr } = getColumnPadding(j, undefined, 'thead')
177
+ <div
178
+ style={{ borderSpacing: `0 ${rowGap}px` }}
179
+ className={twMerge('table w-full border-separate', showLoadingOverlay ? 'relative' : '', tableClassName)}
180
+ >
181
+ {/* Thead row */}
182
+ <div className="table-row relative">
183
+ {
184
+ columns.map((col, j) => {
185
+ const disableSort = col.disableSort || selectedRowIds.length
186
+ const { pl, pr } = getColumnPadding(j, undefined, 'thead')
178
187
 
179
- if (col.isHidden) return <Fragment key={j} />
180
- return (
181
- <div
182
- key={j}
183
- onClick={disableSort ? undefined : () => onSort(col)}
184
- style={{ height: headerHeightMin, minWidth: col.minWidth, paddingLeft: pl, paddingRight: pr }}
185
- className={twMerge(
186
- _columnClassName,
187
- 'h-auto text-sm font-medium border-t-1',
188
- disableSort ? '' : 'cursor-pointer select-none',
189
- getAlignClass(col.align),
190
- columnClassName,
191
- columnHeaderClassName,
192
- columnClassNameFn ? columnClassNameFn(col, undefined, j) : '',
193
- col.className
194
- )}
195
- >
188
+ if (col.isHidden) return <Fragment key={j} />
189
+ return (
196
190
  <div
197
- style={{ maxHeight: rowContentHeightMax }}
191
+ key={j}
192
+ onClick={disableSort ? undefined : () => onSort(col)}
193
+ style={{ height: headerHeightMin, minWidth: col.minWidth, paddingLeft: pl, paddingRight: pr }}
198
194
  className={twMerge(
199
- col.value == 'checkbox' ? 'relative' : getLineClampClassName(col.rowLinesMax),
200
- rowContentHeightMax ? 'overflow-hidden' : '',
201
- col.overflow ? 'overflow-visible' : '',
202
- col.innerClassName
195
+ _columnClassName,
196
+ 'h-auto text-sm font-medium border-t-1',
197
+ disableSort ? '' : 'cursor-pointer select-none',
198
+ getAlignClass(col.align),
199
+ columnClassName,
200
+ columnHeaderClassName,
201
+ columnClassNameFn ? columnClassNameFn(col, undefined, j) : '',
202
+ col.className
203
203
  )}
204
- >
205
- {
206
- col.value == 'checkbox'
207
- ? <Fragment>
208
- <Checkbox
209
- size={checkboxSize}
210
- name={`checkbox-all-${rand}`}
211
- hitboxPadding={5}
212
- checked={rows.length > 0 && selectedRowIds.length === rows.length}
213
- className='!m-0 py-[5px]' // py-5 is required for hitbox (restricted to tabel cell height)
214
- checkboxClassName={twMerge('border-foreground shadow-[0_1px_2px_0px_#0000001c]', checkboxClassName)}
215
- onChange={(e) => onSelect('all', e.target.checked)}
216
- />
217
- <div
218
- className={`${selectedRowIds.length ? 'block' : 'hidden'} [&>*]:absolute [&>*]:inset-y-0 [&>*]:left-[35px] [&>*]:z-10 whitespace-nowrap`}
219
- >
220
- {generateCheckboxActions && generateCheckboxActions(selectedRowIds, setSelectedRowIds)}
221
- </div>
222
- </Fragment>
223
- : <span className={twMerge(
224
- 'flex items-center gap-x-2 transition-opacity',
225
- selectedRowIds.length ? 'opacity-0' : '',
226
- getAlignClass(col.align, true)
227
- )}>
228
- <span>{col.label}</span>
229
- {
230
- (!col.disableSort && sortBy == col.sortByValue)
231
- ? (sort == '1'
232
- ? <ChevronDownIcon class='shrink-0 size-[16px]' />
233
- : <ChevronUpIcon class='shrink-0 size-[16px]' />
234
- )
235
- : col.align == 'left' && <div class='size-[16px] shrink-0' /> // prevent layout shift on sort
236
- }
237
- </span>
238
- }
239
- </div>
240
- </div>
241
- )
242
- })
243
- }
244
- </div>
245
- {/* Tbody rows */}
246
- {
247
- rowsToRender.map((row: T, i: number) => {
248
- const isSelected = selectedRowIds.includes(row._id || '')
249
- const Element = (rowLink ? Link : 'div') as React.ElementType
250
- const extraProps = rowLink ? { to: rowLink(row) } : { onClick: rowOnClick ? () => rowOnClick(row) : undefined }
251
- return (
252
- <Element
253
- {...extraProps}
254
- key={`${row._id}-${i}`}
255
- id={`row-${row._id}-${i}`}
256
- className={twMerge(
257
- `table-row relative ${(rowOnClick || rowLink) ? 'cursor-pointer' : ''} ${isSelected ? 'is-selected' : ''}`,
258
- rowClassName,
259
- rowClassNameFn ? rowClassNameFn(row, i) : ''
260
- )}
261
- >
262
- {columns.map((col, j) => {
263
- const rowType = row._id ? 'row' : isLoading ? 'loading' : 'empty'
264
- const { pl, pr, sideColor } = getColumnPadding(j, isLoading ? undefined : row, rowType)
265
- if (col.isHidden) return <Fragment key={j} />
266
- return (
204
+ >
267
205
  <div
268
- key={j}
269
- style={{ height: rowHeightMin, paddingLeft: pl, paddingRight: pr }}
206
+ style={{ maxHeight: rowContentHeightMax }}
270
207
  className={twMerge(
271
- _columnClassName,
272
- getAlignClass(col.align),
273
- columnClassName,
274
- columnClassNameFn ? columnClassNameFn(col, row, i) : '',
275
- col.className,
276
- isSelected ? `bg-gray-50 ${columnSelectedClassName||''}` : ''
208
+ col.value == 'checkbox' ? 'relative' : getLineClampClassName(col.rowLinesMax),
209
+ rowContentHeightMax ? 'overflow-hidden' : '',
210
+ col.overflow ? 'overflow-visible' : '',
211
+ col.innerClassName
277
212
  )}
278
- >
279
- <div
280
- // pl:sideColorPadding was originally here
281
- style={{ maxHeight: rowContentHeightMax }}
213
+ >
214
+ {
215
+ col.value == 'checkbox'
216
+ ? <Fragment>
217
+ <Checkbox
218
+ size={checkboxSize}
219
+ name={`checkbox-all-${rand}`}
220
+ hitboxPadding={5}
221
+ checked={rows.length > 0 && selectedRowIds.length === rows.length}
222
+ className='!m-0 py-[5px]' // py-5 is required for hitbox (restricted to tabel cell height)
223
+ checkboxClassName={twMerge('border-foreground shadow-[0_1px_2px_0px_#0000001c]', checkboxClassName)}
224
+ onChange={(e) => onSelect('all', e.target.checked)}
225
+ />
226
+ <div
227
+ className={`${selectedRowIds.length ? 'block' : 'hidden'} [&>*]:absolute [&>*]:inset-y-0 [&>*]:left-[35px] [&>*]:z-10 whitespace-nowrap`}
228
+ >
229
+ {generateCheckboxActions && generateCheckboxActions(selectedRowIds, setSelectedRowIds)}
230
+ </div>
231
+ </Fragment>
232
+ : <span className={twMerge(
233
+ 'flex items-center gap-x-2 transition-opacity',
234
+ selectedRowIds.length ? 'opacity-0' : '',
235
+ getAlignClass(col.align, true)
236
+ )}>
237
+ <span>{col.label}</span>
238
+ {
239
+ (!col.disableSort && sortBy == col.sortByValue)
240
+ ? (sort == '1'
241
+ ? <ChevronDownIcon class='shrink-0 size-[16px]' />
242
+ : <ChevronUpIcon class='shrink-0 size-[16px]' />
243
+ )
244
+ : col.align == 'left' && <div class='size-[16px] shrink-0' /> // prevent layout shift on sort
245
+ }
246
+ </span>
247
+ }
248
+ </div>
249
+ </div>
250
+ )
251
+ })
252
+ }
253
+ </div>
254
+ {/* Tbody rows */}
255
+ {
256
+ rowsToRender.map((row: T, i: number) => {
257
+ const isSelected = selectedRowIds.includes(row._id || '')
258
+ const Element = (rowLink ? Link : 'div') as React.ElementType
259
+ const extraProps = rowLink ? { to: rowLink(row) } : { onClick: rowOnClick ? () => rowOnClick(row) : undefined }
260
+ return (
261
+ <Element
262
+ {...extraProps}
263
+ key={`${row._id}-${i}`}
264
+ id={`row-${row._id}-${i}`}
265
+ className={twMerge(
266
+ `table-row relative ${(rowOnClick || rowLink) ? 'cursor-pointer' : ''} ${isSelected ? 'is-selected' : ''}`,
267
+ rowClassName,
268
+ rowClassNameFn ? rowClassNameFn(row, i) : ''
269
+ )}
270
+ >
271
+ {columns.map((col, j) => {
272
+ const rowType = row._id ? 'row' : isLoading ? 'loading' : 'empty'
273
+ const { pl, pr, sideColor } = getColumnPadding(j, isLoading ? undefined : row, rowType)
274
+ if (col.isHidden) return <Fragment key={j} />
275
+ return (
276
+ <div
277
+ key={j}
278
+ style={{ height: rowHeightMin, paddingLeft: pl, paddingRight: pr }}
282
279
  className={twMerge(
283
- rowContentHeightMax ? 'overflow-hidden' : '',
284
- getLineClampClassName(col.rowLinesMax),
285
- col.overflow ? 'overflow-visible' : '',
286
- col.innerClassName
280
+ _columnClassName,
281
+ getAlignClass(col.align),
282
+ columnClassName,
283
+ columnClassNameFn ? columnClassNameFn(col, row, i) : '',
284
+ col.className,
285
+ isSelected ? `bg-gray-50 ${columnSelectedClassName||''}` : ''
287
286
  )}
288
287
  >
289
- {
290
- // Side color
291
- sideColor &&
292
- <div
293
- className={`absolute top-0 left-0 h-full ${sideColor?.className||''}`}
294
- style={{ width: sideColor.width }}
295
- />
296
- }
297
- {
298
- // Rows (content hidden when loading inline)
299
- row._id &&
300
- <div className={isLoading && showLoadingInline ? 'opacity-0 pointer-events-none' : ''}>
301
- {
302
- col.value == 'checkbox'
303
- ? <Checkbox
304
- size={checkboxSize}
305
- name={`checkbox-${row._id}`}
306
- onChange={(e) => onSelect(row?._id || '', e.target.checked)}
307
- checked={selectedRowIds.includes(row?._id || '')}
308
- onClick={(e) => e.stopPropagation()}
309
- hitboxPadding={5}
310
- className='!m-0 py-[5px]' // py-5 is required for hitbox (restricted to tabel cell height)
311
- checkboxClassName={twMerge('border-foreground shadow-[0_1px_2px_0px_#0000001c]', checkboxClassName)}
312
- />
313
- : generateTd(col, row, i, i == rows.length - 1)
314
- }
315
- </div>
316
- }
317
- {
318
- // Show "no records" or "loading" text in the first column
319
- j == 0 && (!row._id || isLoading) &&
320
- <div className={'absolute top-0 h-full flex items-center justify-center gap-3 text-sm text-gray-500'}>
321
- {
322
- (!row._id && !isLoading) ? (
323
- 'No records found.'
324
- ) : (!row._id && isLoading && showLoadingInline === true) ? (
325
- <Fragment>
326
- <Spinner className="border-gray-500" />
327
- <LoadingWithDots message={loadingMessage} />
328
- </Fragment>
329
- ) : (!row._id && isLoading && showLoadingInline) ? (
330
- showLoadingInline
331
- ) : null
332
- }
333
- </div>
334
- }
288
+ <div
289
+ // pl:sideColorPadding was originally here
290
+ style={{ maxHeight: rowContentHeightMax }}
291
+ className={twMerge(
292
+ rowContentHeightMax ? 'overflow-hidden' : '',
293
+ getLineClampClassName(col.rowLinesMax),
294
+ col.overflow ? 'overflow-visible' : '',
295
+ col.innerClassName
296
+ )}
297
+ >
298
+ {
299
+ // Side color
300
+ sideColor &&
301
+ <div
302
+ className={`absolute top-0 left-0 h-full ${sideColor?.className||''}`}
303
+ style={{ width: sideColor.width }}
304
+ />
305
+ }
306
+ {
307
+ // Rows (content hidden when loading inline)
308
+ row._id &&
309
+ <div className={isLoading && showLoadingInline ? 'opacity-0 pointer-events-none' : ''}>
310
+ {
311
+ col.value == 'checkbox'
312
+ ? <Checkbox
313
+ size={checkboxSize}
314
+ name={`checkbox-${row._id}`}
315
+ onChange={(e) => onSelect(row?._id || '', e.target.checked)}
316
+ checked={selectedRowIds.includes(row?._id || '')}
317
+ onClick={(e) => e.stopPropagation()}
318
+ hitboxPadding={5}
319
+ className='!m-0 py-[5px]' // py-5 is required for hitbox (restricted to tabel cell height)
320
+ checkboxClassName={twMerge('border-foreground shadow-[0_1px_2px_0px_#0000001c]', checkboxClassName)}
321
+ />
322
+ : generateTd(col, row, i, i == rows.length - 1)
323
+ }
324
+ </div>
325
+ }
326
+ {
327
+ // Show "no records" or "loading" text in the first column
328
+ j == 0 && (!row._id || isLoading) &&
329
+ <div className={'absolute top-0 h-full flex items-center justify-center gap-3 text-sm text-gray-500'}>
330
+ {
331
+ (!row._id && !isLoading) ? (
332
+ 'No records found.'
333
+ ) : (!row._id && isLoading && showLoadingInline === true) ? (
334
+ <Fragment>
335
+ <Spinner className="border-gray-500" />
336
+ <LoadingWithDots message={loadingMessage} />
337
+ </Fragment>
338
+ ) : (!row._id && isLoading && showLoadingInline) ? (
339
+ showLoadingInline
340
+ ) : null
341
+ }
342
+ </div>
343
+ }
344
+ </div>
335
345
  </div>
336
- </div>
337
- )
338
- })}
339
- </Element>
340
- )
341
- })
342
- }
343
- {
344
- (isLoading && showLoadingOverlay === true)
345
- ? <LoadingOverlay className={twMerge('m-[1px]', loadingOverlayClassName)} message={loadingMessage} />
346
- : (isLoading && showLoadingOverlay) ? showLoadingOverlay : null
347
- }
346
+ )
347
+ })}
348
+ </Element>
349
+ )
350
+ })
351
+ }
352
+ {
353
+ (isLoading && showLoadingOverlay === true)
354
+ ? <LoadingOverlay className={twMerge('m-[1px]', loadingOverlayClassName)} message={loadingMessage} />
355
+ : (isLoading && showLoadingOverlay) ? showLoadingOverlay : null
356
+ }
357
+ </div>
348
358
  </div>
349
359
  </div>
350
360
  )
@@ -220,7 +220,10 @@ export function Styleguide({ className, elements, children, currencies, groups }
220
220
  case 'actions':
221
221
  return (
222
222
  <Dropdown
223
- options={[{ label: 'Set Status' }, { label: 'Delete' }]}
223
+ options={[
224
+ { label: 'Set Status', onClick: () => { console.log('set', row._id) } },
225
+ { label: 'Delete', onClick: () => { console.log('remove', row._id) } },
226
+ ]}
224
227
  dir={rows.slice(0, perPage).length - 3 < i ? 'top-right' : 'bottom-right'}
225
228
  minWidth={100}
226
229
  >
@@ -701,51 +704,64 @@ export function Styleguide({ className, elements, children, currencies, groups }
701
704
  />
702
705
  </div>
703
706
  <Table
704
- rows={rows.slice(0, perPage)}
707
+ className="mb-6"
705
708
  columns={thead}
706
- rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
707
709
  generateCheckboxActions={generateCheckboxActions}
708
710
  generateTd={generateTd}
709
- className="mb-6"
711
+ rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
712
+ rows={rows.slice(0, perPage)}
710
713
  />
711
714
  <Table
712
- rows={[]}
715
+ className="mb-6"
713
716
  columns={thead}
714
- rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
715
717
  generateCheckboxActions={generateCheckboxActions}
716
718
  generateTd={generateTd}
717
- className="mb-6"
718
719
  isLoading={true}
720
+ rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
721
+ rows={[]}
719
722
  />
720
723
  <Table
721
- rows={[]}
724
+ className="mb-6"
722
725
  columns={thead}
723
- rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
724
726
  generateCheckboxActions={generateCheckboxActions}
725
727
  generateTd={generateTd}
726
- className="mb-6"
727
728
  isLoading={true}
728
- showLoadingOverlay={false}
729
+ rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
730
+ rows={[]}
729
731
  showLoadingInline={true}
732
+ showLoadingOverlay={false}
730
733
  />
731
734
  <Table
732
- rows={rows.slice(0, 2).map(row => ({ ...row, _id: row._id + '1' }))}
735
+ className="mb-6"
733
736
  columns={thead}
734
- rowLinesMax={1}
737
+ generateCheckboxActions={generateCheckboxActions}
738
+ generateTd={generateTd}
739
+ rowLink={(row) => `#/table-link/${row?._id}`}
740
+ rowSideColor={(row) => ({ className: row?.status == 'pending' ? 'bg-yellow-400' : '', width: 5 })}
741
+ rows={rows.slice(0, 1).map(row => ({ ...row, _id: '1_' + row._id }))}
742
+ />
743
+ <Table
744
+ className="mb-6"
745
+ columns={thead}
746
+ generateCheckboxActions={generateCheckboxActions}
747
+ generateTd={generateTd}
735
748
  headerHeightMin={35}
736
749
  rowGap={8}
737
750
  rowHeightMin={42}
751
+ rowLinesMax={1}
752
+ rowOnClick={useCallback((row: QuoteExample) => {
753
+ console.log('row clicked', row?._id)
754
+ setStore((s) => ({ ...s, message: `Row ${row?._id} clicked` }))
755
+ }, [setStore])}
738
756
  rowSideColor={(row) => ({ className: `rounded-l-xl ${statusColors(row?.status as string)}`, width: 10 })}
739
- rowOnClick={useCallback((row: QuoteExample) => {setStore((s) => ({ ...s, message: `Row ${row?._id} clicked` }))}, [setStore])}
740
- generateCheckboxActions={generateCheckboxActions}
741
- generateTd={generateTd}
742
- className="mb-5"
743
- tableClassName="rounded-3px"
744
- rowClassName="[&:hover>div]:bg-gray-50"
757
+ rows={rows.slice(0, 2).map(row => ({ ...row, _id: '2_' + row._id }))}
758
+
759
+ checkboxClassName="rounded-[2px] shadow-none"
745
760
  columnClassName="border-t-1 first:rounded-l-xl last:rounded-r-xl"
746
- columnSelectedClassName="bg-gray-50 border-indigo-300"
747
761
  columnHeaderClassName="text-gray-500 text-2xs uppercase border-none"
748
- checkboxClassName="rounded-[2px] shadow-none"
762
+ columnSelectedClassName="bg-gray-50 border-indigo-300"
763
+ rowClassName="[&:hover>div]:bg-gray-50"
764
+ tableClassName="rounded-3px"
749
765
  />
750
766
  </div>
751
767
  )}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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 🚀",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@stripe/stripe-js": "^1.34.0",
67
- "monastery": "^4.0.1",
67
+ "monastery": "^4.0.6",
68
68
  "stripe": "^9.16.0"
69
69
  },
70
70
  "_peers-are-packages-that-will-be-used-in-the-host-repo-too": "",
package/server/router.js CHANGED
@@ -9,7 +9,7 @@ import expressFileUpload from 'express-fileupload'
9
9
  import express from 'express'
10
10
  import bodyParser from 'body-parser'
11
11
  import sortRouteAddressesNodeps from 'sort-route-addresses-nodeps'
12
-
12
+ import { formatDuplicateKeyError } from 'monastery'
13
13
  import { sendEmail } from 'nitro-web/server'
14
14
  import * as util from 'nitro-web/util'
15
15
 
@@ -177,17 +177,6 @@ function setupErrorResponses (expressApp) {
177
177
  serverError: function(a, b) { error.call(this, a, b, 500) },
178
178
  })
179
179
 
180
- function duplicateKeyIndexAndValue(error) {
181
- // https://github.com/Automattic/mongoose/issues/2129#issuecomment-280507821
182
- // E.g. E11000 duplicate key error collection: anamata-production.person index:
183
- // email_1 dup key: { email: "person1@gmail.com" }
184
- let regex = /index: (?:.*\.)?\$?(?:([_a-z0-9]*)(?:_\d*)|([_a-z0-9]*))\s*dup key/i
185
- let match = error.message.match(regex)
186
- let index = match[1] || match[2]
187
- let value = (error.message.match(/.*{.*?: (.*) }/i)[1]||'').replace(/"/g, '')
188
- return [index, value]
189
- }
190
-
191
180
  function error(error, detail, status) {
192
181
  /**
193
182
  * Returns a formatted error
@@ -218,11 +207,15 @@ function setupErrorResponses (expressApp) {
218
207
  else errors = [{ detail: error || _detail }]
219
208
 
220
209
  // Mongo error
221
- } else if (util.isObject(error) && (error.name||'').match(/Mongo|BulkWriteError/)) {
222
- if (error.code == 11000) {
223
- let [name] = duplicateKeyIndexAndValue(error)
224
- if (name == 'email') errors = [{ title: 'email', detail: 'That email is already linked to an account.' }]
225
- else errors = [{ title: name, detail: `Cannot insert duplicate values for "${name}".` }]
210
+ } else if (util.isObject(error) && (error.name || '').match(/Mongo|BulkWriteError/)) {
211
+ // Below is thrown by Monastery if a custom message is not used, e.g. `{ ..., messages : { email: { unique: '...' } } }`
212
+ const formattedDuplicateKeyError = formatDuplicateKeyError(undefined, error)
213
+ if (formattedDuplicateKeyError) {
214
+ errors = formattedDuplicateKeyError
215
+ const hasRawMessage = errors[0].detail.includes('E11000 duplicate key')
216
+ // Add default messages if no custom message is used from definition.messages
217
+ if (hasRawMessage && errors[0].title == 'email') errors[0].detail = 'That email is already linked to an account.'
218
+ else if (hasRawMessage) errors[0].detail = `Cannot insert duplicate values for "${errors[0].title}".`
226
219
  } else {
227
220
  errors = [{ title: 'mongo', detail: error.message }]
228
221
  }
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../server/router.js"],"names":[],"mappings":"AAoCA,uGAAuG;AACvG,0CADc,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,SAAS,EAAE,WAAW,CAAA;CAAE,CAAC,CA+HlG;AAyPD,+CAEC;AAED,kEAYC;AAzGD,+BAA+B;AAC/B,yBADW,gBAAgB,CAuF1B;sBA1YY,OAAO,CAAC,OAAO,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,GAAoB,CAAC;CAC7B;uBACS,OAAO,CAAC,QAAQ,GAAG;IAC3B,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACvD,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACpD,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACnD,WAAW,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;CACvD;+BACS;IACR,KAAK,EAAE,MAAM,EAAE,CAAC;IACpB,CAAK,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,UAAU,KAAK,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;CACnF;oBAtBgB,SAAS"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../server/router.js"],"names":[],"mappings":"AAoCA,uGAAuG;AACvG,0CADc,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,SAAS,EAAE,WAAW,CAAA;CAAE,CAAC,CA+HlG;AAkPD,+CAEC;AAED,kEAYC;AAzGD,+BAA+B;AAC/B,yBADW,gBAAgB,CAuF1B;sBAnYY,OAAO,CAAC,OAAO,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,GAAoB,CAAC;CAC7B;uBACS,OAAO,CAAC,QAAQ,GAAG;IAC3B,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACvD,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACpD,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;IACnD,WAAW,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC;CACvD;+BACS;IACR,KAAK,EAAE,MAAM,EAAE,CAAC;IACpB,CAAK,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,UAAU,KAAK,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;CACnF;oBAtBgB,SAAS"}