@asteby/metacore-runtime-react 42.0.0 → 43.0.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/api-context.d.ts.map +1 -1
  3. package/dist/api-context.js +6 -4
  4. package/dist/dynamic-icon.d.ts.map +1 -1
  5. package/dist/dynamic-icon.js +9 -13
  6. package/dist/dynamic-kanban.d.ts +83 -6
  7. package/dist/dynamic-kanban.d.ts.map +1 -1
  8. package/dist/dynamic-kanban.js +647 -146
  9. package/dist/dynamic-table.d.ts +1 -1
  10. package/dist/dynamic-table.d.ts.map +1 -1
  11. package/dist/dynamic-table.js +14 -1
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -1
  15. package/dist/license/license-gate.d.ts +0 -21
  16. package/dist/license/license-gate.d.ts.map +1 -1
  17. package/dist/license/license-gate.js +5 -2
  18. package/dist/portal-drag-overlay.d.ts +5 -0
  19. package/dist/portal-drag-overlay.d.ts.map +1 -0
  20. package/dist/portal-drag-overlay.js +8 -0
  21. package/dist/query-batch.d.ts.map +1 -1
  22. package/dist/query-batch.js +6 -3
  23. package/dist/sidebar-layout-manager.js +3 -2
  24. package/dist/use-flip-animation.d.ts +6 -0
  25. package/dist/use-flip-animation.d.ts.map +1 -1
  26. package/dist/use-flip-animation.js +11 -4
  27. package/package.json +4 -3
  28. package/src/__tests__/dynamic-icon-slot.test.tsx +50 -0
  29. package/src/__tests__/dynamic-kanban-lane-reorder.test.tsx +2 -1
  30. package/src/__tests__/dynamic-kanban-pipelines-pro.test.tsx +388 -0
  31. package/src/__tests__/dynamic-kanban.test.tsx +15 -2
  32. package/src/__tests__/image-cell-lucide.test.tsx +7 -5
  33. package/src/api-context.tsx +7 -5
  34. package/src/dynamic-icon.tsx +12 -16
  35. package/src/dynamic-kanban.tsx +950 -262
  36. package/src/dynamic-table.tsx +16 -2
  37. package/src/index.ts +6 -0
  38. package/src/license/license-gate.tsx +6 -2
  39. package/src/portal-drag-overlay.tsx +20 -0
  40. package/src/query-batch.ts +6 -3
  41. package/src/sidebar-layout-manager.tsx +3 -3
  42. package/src/use-flip-animation.ts +16 -3
@@ -12,6 +12,7 @@
12
12
  // column cells are tightly coupled to their design system).
13
13
  import { useEffect, useState, useMemo, useCallback, useRef, type MouseEvent } from 'react'
14
14
  import { useTranslation } from 'react-i18next'
15
+ import { useTimeZone, useCurrency } from './org-runtime-context'
15
16
  import { format } from 'date-fns'
16
17
  import type { DateRange } from 'react-day-picker'
17
18
  import { useVirtualizer } from '@tanstack/react-virtual'
@@ -35,6 +36,7 @@ import {
35
36
  useReactTable,
36
37
  } from '@tanstack/react-table'
37
38
  import { cn } from '@asteby/metacore-ui/lib'
39
+ import { preloadGlyphs } from '@asteby/metacore-ui/icons'
38
40
  import {
39
41
  Table,
40
42
  TableBody,
@@ -282,12 +284,18 @@ export function DynamicTable({
282
284
  defaultFilters,
283
285
  extraColumns = [],
284
286
  getDynamicColumns = defaultGetDynamicColumns,
285
- timeZone,
286
- currency,
287
+ timeZone: timeZoneProp,
288
+ currency: currencyProp,
287
289
  pagination: paginationMode,
288
290
  infiniteScroll: infiniteScrollProp = false,
289
291
  virtualizeRows,
290
292
  }: DynamicTableProps) {
293
+ // The org's timezone/currency: an explicit prop wins, else the app-wide
294
+ // OrgRuntimeProvider (without it, money fell back to USD).
295
+ const orgTimeZone = useTimeZone()
296
+ const orgCurrency = useCurrency()
297
+ const timeZone = timeZoneProp ?? orgTimeZone
298
+ const currency = currencyProp ?? orgCurrency
291
299
  // The explicit `pagination` prop wins; the legacy `infiniteScroll` boolean
292
300
  // still selects the mode when `pagination` is absent (back-compat).
293
301
  const infiniteScroll = paginationMode ? paginationMode === 'infinite' : infiniteScrollProp
@@ -754,6 +762,12 @@ export function DynamicTable({
754
762
  return gateTableMetadata(metadata, model, can, (key, fallback) => t(key, { defaultValue: fallback }))
755
763
  }, [metadata, permissionsActive, can, model, t])
756
764
 
765
+ // Row-action menus mount their icons on open: load them with the
766
+ // metadata so the first open draws every glyph at once.
767
+ useEffect(() => {
768
+ void preloadGlyphs((metadata?.actions ?? []).map((a) => a.icon))
769
+ }, [metadata])
770
+
757
771
  // Importing is offered when the host opts in explicitly (`canImport`) or,
758
772
  // when it says nothing, whenever the kernel served an import spec for the
759
773
  // model — which it does for every model with at least one importable form
package/src/index.ts CHANGED
@@ -57,9 +57,15 @@ export {
57
57
  groupByStage,
58
58
  isTransitionAllowed,
59
59
  applyOptimisticMove,
60
+ applyStageOverrides,
61
+ laneDeltasFromOverrides,
60
62
  selectCardColumns,
63
+ visibleCardFields,
64
+ isEmptyCardValue,
65
+ nextLaneCoordinates,
61
66
  UNASSIGNED_LANE,
62
67
  } from './dynamic-kanban'
68
+ export { PortalDragOverlay, type PortalDragOverlayProps } from './portal-drag-overlay'
63
69
  export {
64
70
  useStageAutomations,
65
71
  StageAutomationsButton,
@@ -19,6 +19,7 @@
19
19
  * INDEPENDIENTE del kernel: no importa su cliente ni fija su versión. Solo
20
20
  * consume `LicenseState` y una promesa `onActivate`.
21
21
  */
22
+ import { createPortal } from 'react-dom'
22
23
  import { useState, type ReactNode } from 'react'
23
24
  import { useTranslation } from 'react-i18next'
24
25
  import { ShieldAlert, Clock, Loader2 } from 'lucide-react'
@@ -165,7 +166,9 @@ function LicenseGateModal({
165
166
  }
166
167
  }
167
168
 
168
- return (
169
+ // In <body>: mounted inside a glass panel (backdrop-filter), a fixed veil
170
+ // would cover only that panel.
171
+ return createPortal(
169
172
  <div
170
173
  role="dialog"
171
174
  aria-modal="true"
@@ -300,6 +303,7 @@ function LicenseGateModal({
300
303
  )}
301
304
  </div>
302
305
  </div>
303
- </div>
306
+ </div>,
307
+ document.body,
304
308
  )
305
309
  }
@@ -0,0 +1,20 @@
1
+ // PortalDragOverlay — dnd-kit's <DragOverlay> rendered into <body>.
2
+ //
3
+ // The overlay is `position: fixed` at the viewport coordinates of the item
4
+ // being dragged. Any ancestor with a transform, filter or backdrop-filter
5
+ // becomes the containing block of a fixed descendant, so inside such an
6
+ // ancestor (a glass panel from @asteby/metacore-theme/glass.css, an animated
7
+ // dialog) the lifted item shows up shifted by that ancestor's offset instead of
8
+ // under the pointer. Rendering it into <body> keeps the viewport as its
9
+ // containing block wherever the board or list is mounted. React context (the
10
+ // DndContext) still reaches it through the portal.
11
+ import * as React from 'react'
12
+ import { createPortal } from 'react-dom'
13
+ import { DragOverlay } from '@dnd-kit/core'
14
+
15
+ export type PortalDragOverlayProps = React.ComponentProps<typeof DragOverlay>
16
+
17
+ export function PortalDragOverlay(props: PortalDragOverlayProps) {
18
+ if (typeof document === 'undefined') return null
19
+ return createPortal(<DragOverlay {...props} />, document.body)
20
+ }
@@ -377,8 +377,11 @@ function batchForbidden(message?: string): boolean {
377
377
  * or options read. Anything the batch refuses falls back to the original GET.
378
378
  */
379
379
  export async function batchGet(api: ApiClient, url: string, config?: { params?: Record<string, unknown> }) {
380
+ // The fallback GET gets exactly the arguments the caller passed: no
381
+ // trailing `undefined` config reaching the host client.
382
+ const plainGet = () => (config === undefined ? api.get(url) : api.get(url, config))
380
383
  const token = tokenForGet(url, config?.params)
381
- if (!token) return api.get(url, config)
384
+ if (!token) return plainGet()
382
385
  try {
383
386
  const part = await loadQueryPart(api, token)
384
387
  if (!part.success) {
@@ -391,13 +394,13 @@ export async function batchGet(api: ApiClient, url: string, config?: { params?:
391
394
  err.response = { status: 403 }
392
395
  throw err
393
396
  }
394
- return api.get(url, config)
397
+ return plainGet()
395
398
  }
396
399
  return { data: { success: true, data: part.data, meta: part.meta } }
397
400
  } catch (err) {
398
401
  const status = (err as { status?: number; response?: { status?: number } })?.status
399
402
  ?? (err as { response?: { status?: number } })?.response?.status
400
403
  if (status === 403) throw err
401
- return api.get(url, config)
404
+ return plainGet()
402
405
  }
403
406
  }
@@ -5,7 +5,6 @@
5
5
  import * as React from 'react'
6
6
  import {
7
7
  DndContext,
8
- DragOverlay,
9
8
  PointerSensor,
10
9
  closestCenter,
11
10
  useSensor,
@@ -20,6 +19,7 @@ import {
20
19
  verticalListSortingStrategy,
21
20
  } from '@dnd-kit/sortable'
22
21
  import { CSS } from '@dnd-kit/utilities'
22
+ import { PortalDragOverlay } from './portal-drag-overlay'
23
23
  import {
24
24
  ChevronDown,
25
25
  ChevronRight,
@@ -733,13 +733,13 @@ export function SidebarLayoutManager({
733
733
  ) : (
734
734
  renderList(tree, [])
735
735
  )}
736
- <DragOverlay>
736
+ <PortalDragOverlay>
737
737
  {activeLabel ? (
738
738
  <div className="bg-background border-border rounded-md border px-3 py-1.5 text-sm shadow-lg">
739
739
  {activeLabel}
740
740
  </div>
741
741
  ) : null}
742
- </DragOverlay>
742
+ </PortalDragOverlay>
743
743
  </DndContext>
744
744
 
745
745
  <div className="mt-3 flex gap-2 px-2">
@@ -23,6 +23,12 @@ export interface UseFlipAnimationOptions {
23
23
  easing?: MotionEasing | string
24
24
  /** Off switch; the hook also stays still under prefers-reduced-motion. */
25
25
  disabled?: boolean
26
+ /**
27
+ * Leave one element still on this change while still tracking where it is
28
+ * (e.g. the card a drag overlay is already carrying into place). Default:
29
+ * animate everything.
30
+ */
31
+ shouldAnimate?: (el: HTMLElement, key: string) => boolean
26
32
  }
27
33
 
28
34
  const DEFAULT_SELECTOR = '[data-flip-key]'
@@ -54,12 +60,13 @@ export function useFlipAnimation(
54
60
  duration = 'moderate',
55
61
  easing = 'standard',
56
62
  disabled = false,
63
+ shouldAnimate,
57
64
  } = options
58
65
  const positionsRef = useRef<Positions | null>(null)
59
66
  const triggerRef = useRef(trigger)
60
- const configRef = useRef({ selector, keyOf, scrollContainer })
67
+ const configRef = useRef({ selector, keyOf, scrollContainer, shouldAnimate })
61
68
  useEffect(() => {
62
- configRef.current = { selector, keyOf, scrollContainer }
69
+ configRef.current = { selector, keyOf, scrollContainer, shouldAnimate }
63
70
  })
64
71
 
65
72
  const measure = (): Positions | null => {
@@ -105,11 +112,16 @@ export function useFlipAnimation(
105
112
  schedule()
106
113
  document.addEventListener('pointerup', onInteraction, true)
107
114
  document.addEventListener('keyup', onInteraction, true)
115
+ // Scrolling a list inside the root moves its items without a trigger
116
+ // change; without a fresh snapshot the next change would replay the scroll
117
+ // distance as movement. (Scroll does not bubble: listen in capture.)
118
+ document.addEventListener('scroll', onInteraction, true)
108
119
  window.addEventListener('resize', schedule)
109
120
  return () => {
110
121
  if (timer) clearTimeout(timer)
111
122
  document.removeEventListener('pointerup', onInteraction, true)
112
123
  document.removeEventListener('keyup', onInteraction, true)
124
+ document.removeEventListener('scroll', onInteraction, true)
113
125
  window.removeEventListener('resize', schedule)
114
126
  }
115
127
  }, [rootRef])
@@ -127,7 +139,7 @@ export function useFlipAnimation(
127
139
  if (durationMs <= 0) return
128
140
  const timing = easing in MOTION_EASING_KEYS ? motionEasing(easing as MotionEasing) : easing
129
141
 
130
- const { selector: sel, keyOf: key } = configRef.current
142
+ const { selector: sel, keyOf: key, shouldAnimate: animates } = configRef.current
131
143
  // A stale snapshot can report huge jumps; those would read as a glitch.
132
144
  const maxJump = root.clientHeight > 0 ? root.clientHeight * 1.5 : Infinity
133
145
  root.querySelectorAll<HTMLElement>(sel).forEach((el) => {
@@ -136,6 +148,7 @@ export function useFlipAnimation(
136
148
  const from = before.get(k)
137
149
  const to = after.get(k)
138
150
  if (!to || typeof el.animate !== 'function') return
151
+ if (animates && !animates(el, k)) return
139
152
  if (!from) {
140
153
  // Entered with this change: fade in instead of popping.
141
154
  el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durationMs, easing: timing })