@asteby/metacore-runtime-react 23.6.0 → 23.7.1
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/CHANGELOG.md +25 -0
- package/dist/action-modal-dispatcher.js +19 -10
- package/dist/custom-stages.d.ts +171 -0
- package/dist/custom-stages.d.ts.map +1 -0
- package/dist/custom-stages.js +535 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +7 -4
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +77 -32
- package/dist/field-grid.d.ts +17 -0
- package/dist/field-grid.d.ts.map +1 -0
- package/dist/field-grid.js +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/model-action-toolbar.d.ts.map +1 -1
- package/dist/model-action-toolbar.js +3 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/action-modal-grid-layout.test.tsx +86 -0
- package/src/__tests__/custom-stages.test.tsx +480 -0
- package/src/__tests__/model-action-toolbar-i18n.test.tsx +101 -0
- package/src/action-modal-dispatcher.tsx +44 -37
- package/src/custom-stages.tsx +1113 -0
- package/src/dialogs/dynamic-record.tsx +14 -8
- package/src/dynamic-kanban.tsx +153 -5
- package/src/field-grid.tsx +57 -0
- package/src/index.ts +25 -0
- package/src/model-action-toolbar.tsx +9 -1
- package/src/types.ts +26 -0
|
@@ -52,6 +52,7 @@ import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-f
|
|
|
52
52
|
import { DynamicRelations } from '../dynamic-relations'
|
|
53
53
|
import { useOptionsResolver, type ResolvedOption } from '../use-options-resolver'
|
|
54
54
|
import { getFieldRef } from '../dynamic-form-schema'
|
|
55
|
+
import { FieldCell } from '../field-grid'
|
|
55
56
|
import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
|
|
56
57
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
|
|
57
58
|
import { humanizeToken } from '../dynamic-columns-helpers'
|
|
@@ -767,18 +768,22 @@ export function DynamicRecordDialog({
|
|
|
767
768
|
<ImageUrlContext.Provider value={getImageUrl}>
|
|
768
769
|
<TimeZoneContext.Provider value={timeZone}>
|
|
769
770
|
<CurrencyContext.Provider value={currency}>
|
|
771
|
+
{/* The grid IS the form element (the footer submit
|
|
772
|
+
button targets it by id). FieldCell gives each
|
|
773
|
+
cell `min-w-0` so a long select/input value can't
|
|
774
|
+
blow the two columns past the dialog width. */}
|
|
770
775
|
<form
|
|
771
776
|
id="dynamic-record-form"
|
|
772
777
|
onSubmit={handleSubmit}
|
|
773
|
-
className="grid grid-cols-1
|
|
778
|
+
className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2"
|
|
774
779
|
>
|
|
775
780
|
{visibleFields.map(field => {
|
|
776
|
-
const isFullWidth =
|
|
781
|
+
const isFullWidth =
|
|
782
|
+
field.type === 'textarea' ||
|
|
783
|
+
field.widget === 'textarea' ||
|
|
784
|
+
field.widget === 'richtext'
|
|
777
785
|
return (
|
|
778
|
-
<
|
|
779
|
-
key={field.key}
|
|
780
|
-
className={isFullWidth ? 'sm:col-span-2' : ''}
|
|
781
|
-
>
|
|
786
|
+
<FieldCell key={field.key} fullWidth={isFullWidth}>
|
|
782
787
|
<FieldRow
|
|
783
788
|
field={field}
|
|
784
789
|
record={record}
|
|
@@ -788,12 +793,12 @@ export function DynamicRecordDialog({
|
|
|
788
793
|
setFormValues((prev: Record<string, any>) => ({ ...prev, [field.key]: val }))
|
|
789
794
|
}
|
|
790
795
|
/>
|
|
791
|
-
</
|
|
796
|
+
</FieldCell>
|
|
792
797
|
)
|
|
793
798
|
})}
|
|
794
799
|
|
|
795
800
|
{record?.external_url && (
|
|
796
|
-
<div className="sm:col-span-2">
|
|
801
|
+
<div className="sm:col-span-2 min-w-0">
|
|
797
802
|
<a
|
|
798
803
|
href={record.external_url}
|
|
799
804
|
target="_blank"
|
|
@@ -807,6 +812,7 @@ export function DynamicRecordDialog({
|
|
|
807
812
|
)}
|
|
808
813
|
</form>
|
|
809
814
|
|
|
815
|
+
|
|
810
816
|
{/* Child records (line items, etc.) for declared relations.
|
|
811
817
|
View = strictly read-only; edit = add/edit/delete. */}
|
|
812
818
|
{!isCreate && record && relations.length > 0 && (
|
package/src/dynamic-kanban.tsx
CHANGED
|
@@ -85,6 +85,18 @@ import {
|
|
|
85
85
|
type StageAutomation,
|
|
86
86
|
type NewStageAutomation,
|
|
87
87
|
} from './stage-automations'
|
|
88
|
+
import {
|
|
89
|
+
useCustomStages,
|
|
90
|
+
splitCustomStages,
|
|
91
|
+
mergeLaneStages,
|
|
92
|
+
resolveSmartLanes,
|
|
93
|
+
AddStageColumn,
|
|
94
|
+
CustomStageLaneMenu,
|
|
95
|
+
CustomStageDialog,
|
|
96
|
+
CustomStageDeleteDialog,
|
|
97
|
+
SmartLane,
|
|
98
|
+
type CustomStage,
|
|
99
|
+
} from './custom-stages'
|
|
88
100
|
import { useDynamicFilters } from './use-dynamic-filters'
|
|
89
101
|
import {
|
|
90
102
|
FilterChipsRow,
|
|
@@ -463,6 +475,26 @@ export function DynamicKanban({
|
|
|
463
475
|
// the host has no `/stage-automations` endpoint — the ⚡ affordance hides.
|
|
464
476
|
const automations = useStageAutomations(model)
|
|
465
477
|
|
|
478
|
+
// Custom stages (Bitrix-style user-defined columns). Degrades to no-op when
|
|
479
|
+
// the host has no `/custom-stages` endpoint — the "+ Agregar etapa" column
|
|
480
|
+
// and lane menus simply don't render.
|
|
481
|
+
const customStages = useCustomStages(model)
|
|
482
|
+
// Dialog state: create/edit a stage, and the delete confirmation.
|
|
483
|
+
const [stageDialogOpen, setStageDialogOpen] = useState(false)
|
|
484
|
+
const [editingStage, setEditingStage] = useState<CustomStage | null>(null)
|
|
485
|
+
const [deletingStage, setDeletingStage] = useState<CustomStage | null>(null)
|
|
486
|
+
const openCreateStage = useCallback(() => {
|
|
487
|
+
setEditingStage(null)
|
|
488
|
+
setStageDialogOpen(true)
|
|
489
|
+
}, [])
|
|
490
|
+
const openEditStage = useCallback((s: CustomStage) => {
|
|
491
|
+
setEditingStage(s)
|
|
492
|
+
setStageDialogOpen(true)
|
|
493
|
+
}, [])
|
|
494
|
+
const openDeleteStage = useCallback((s: CustomStage) => {
|
|
495
|
+
setDeletingStage(s)
|
|
496
|
+
}, [])
|
|
497
|
+
|
|
466
498
|
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache()
|
|
467
499
|
const cachedMeta = getMetadata(model)
|
|
468
500
|
|
|
@@ -757,10 +789,34 @@ export function DynamicKanban({
|
|
|
757
789
|
[],
|
|
758
790
|
)
|
|
759
791
|
|
|
760
|
-
const
|
|
792
|
+
const declaredStages = useMemo(
|
|
761
793
|
() => (metadata ? deriveStages(metadata) : []),
|
|
762
794
|
[metadata],
|
|
763
795
|
)
|
|
796
|
+
// The kernel merges custom real stages into `metadata.stages` (custom: true)
|
|
797
|
+
// and serves smart lanes in `metadata.smart_lanes` — the metadata is the
|
|
798
|
+
// painting source (ops #704). The CRUD list (`customStages.stages`) only
|
|
799
|
+
// backs the management dialog: it carries the `id`/filters the metadata
|
|
800
|
+
// omits, so we still split it to build `customByKey` (and to fall back when
|
|
801
|
+
// the metadata hasn't caught up yet).
|
|
802
|
+
const { laneStages: customLaneStages, smartStages: crudSmartStages } =
|
|
803
|
+
useMemo(() => splitCustomStages(customStages.stages), [customStages.stages])
|
|
804
|
+
const { lanes: stages, customByKey } = useMemo(
|
|
805
|
+
() => mergeLaneStages(declaredStages, customLaneStages),
|
|
806
|
+
[declaredStages, customLaneStages],
|
|
807
|
+
)
|
|
808
|
+
// Smart lanes to paint: metadata first, folding in CRUD ids by key.
|
|
809
|
+
const smartStages = useMemo(
|
|
810
|
+
() =>
|
|
811
|
+
resolveSmartLanes(metadata?.smart_lanes, crudSmartStages, model),
|
|
812
|
+
[metadata?.smart_lanes, crudSmartStages, model],
|
|
813
|
+
)
|
|
814
|
+
// Position a newly created stage after every existing lane (real + smart).
|
|
815
|
+
const nextStagePosition = useMemo(() => {
|
|
816
|
+
const positions = customStages.stages.map((s) => s.position ?? 0)
|
|
817
|
+
const base = stages.length + smartStages.length
|
|
818
|
+
return Math.max(base, ...positions, 0) + 1
|
|
819
|
+
}, [customStages.stages, stages.length, smartStages.length])
|
|
764
820
|
const transitions = metadata?.transitions
|
|
765
821
|
|
|
766
822
|
const grouped = useMemo(
|
|
@@ -1094,6 +1150,13 @@ export function DynamicKanban({
|
|
|
1094
1150
|
onAutomationCreate={automations.create}
|
|
1095
1151
|
onAutomationUpdate={automations.update}
|
|
1096
1152
|
onAutomationRemove={automations.remove}
|
|
1153
|
+
customStage={
|
|
1154
|
+
customStages.available
|
|
1155
|
+
? customByKey.get(stage.key)
|
|
1156
|
+
: undefined
|
|
1157
|
+
}
|
|
1158
|
+
onEditStage={openEditStage}
|
|
1159
|
+
onDeleteStage={openDeleteStage}
|
|
1097
1160
|
>
|
|
1098
1161
|
{loadingData && cards.length === 0 ? (
|
|
1099
1162
|
<>
|
|
@@ -1123,6 +1186,42 @@ export function DynamicKanban({
|
|
|
1123
1186
|
</KanbanLane>
|
|
1124
1187
|
)
|
|
1125
1188
|
})}
|
|
1189
|
+
|
|
1190
|
+
{/* Smart (virtual) lanes — each runs its own filtered query and
|
|
1191
|
+
renders read-only cards (no drag target). */}
|
|
1192
|
+
{smartStages.map((smart) => (
|
|
1193
|
+
<SmartLane
|
|
1194
|
+
key={`smart-${smart.key}`}
|
|
1195
|
+
stage={smart}
|
|
1196
|
+
model={model}
|
|
1197
|
+
endpoint={endpoint}
|
|
1198
|
+
defaultFilters={defaultFilters}
|
|
1199
|
+
pageSize={pageSize}
|
|
1200
|
+
isDark={isDark}
|
|
1201
|
+
refreshTrigger={refreshTrigger}
|
|
1202
|
+
onEdit={openEditStage}
|
|
1203
|
+
onDelete={openDeleteStage}
|
|
1204
|
+
renderCard={(card) => (
|
|
1205
|
+
<KanbanCard
|
|
1206
|
+
card={card}
|
|
1207
|
+
titleCol={titleCol}
|
|
1208
|
+
fieldCols={fieldCols}
|
|
1209
|
+
actions={rowActions}
|
|
1210
|
+
locale={i18n.language}
|
|
1211
|
+
timeZone={timeZone}
|
|
1212
|
+
currency={currency}
|
|
1213
|
+
onClick={onCardClick}
|
|
1214
|
+
onAction={handleInternalAction}
|
|
1215
|
+
draggable={false}
|
|
1216
|
+
/>
|
|
1217
|
+
)}
|
|
1218
|
+
/>
|
|
1219
|
+
))}
|
|
1220
|
+
|
|
1221
|
+
{/* "+ Agregar etapa" — only when the host wired /custom-stages. */}
|
|
1222
|
+
{customStages.available && (
|
|
1223
|
+
<AddStageColumn onClick={openCreateStage} />
|
|
1224
|
+
)}
|
|
1126
1225
|
</div>
|
|
1127
1226
|
|
|
1128
1227
|
<DragOverlay>
|
|
@@ -1140,6 +1239,35 @@ export function DynamicKanban({
|
|
|
1140
1239
|
|
|
1141
1240
|
{rowActionDialogs}
|
|
1142
1241
|
</DndContext>
|
|
1242
|
+
|
|
1243
|
+
{customStages.available && (
|
|
1244
|
+
<>
|
|
1245
|
+
<CustomStageDialog
|
|
1246
|
+
open={stageDialogOpen}
|
|
1247
|
+
onOpenChange={setStageDialogOpen}
|
|
1248
|
+
model={model}
|
|
1249
|
+
columns={metadata?.columns ?? []}
|
|
1250
|
+
initial={editingStage}
|
|
1251
|
+
nextPosition={nextStagePosition}
|
|
1252
|
+
onCreate={customStages.create}
|
|
1253
|
+
onUpdate={customStages.update}
|
|
1254
|
+
/>
|
|
1255
|
+
<CustomStageDeleteDialog
|
|
1256
|
+
open={!!deletingStage}
|
|
1257
|
+
onOpenChange={(o) => {
|
|
1258
|
+
if (!o) setDeletingStage(null)
|
|
1259
|
+
}}
|
|
1260
|
+
stage={deletingStage}
|
|
1261
|
+
reassignTargets={stages.map((s) => ({
|
|
1262
|
+
key: s.key,
|
|
1263
|
+
label: s.label,
|
|
1264
|
+
}))}
|
|
1265
|
+
onConfirm={(s, reassignTo) =>
|
|
1266
|
+
customStages.remove(s.id, reassignTo)
|
|
1267
|
+
}
|
|
1268
|
+
/>
|
|
1269
|
+
</>
|
|
1270
|
+
)}
|
|
1143
1271
|
</div>
|
|
1144
1272
|
)
|
|
1145
1273
|
}
|
|
@@ -1274,6 +1402,10 @@ interface KanbanLaneProps {
|
|
|
1274
1402
|
patch: Partial<StageAutomation>,
|
|
1275
1403
|
) => Promise<void>
|
|
1276
1404
|
onAutomationRemove: (id: StageAutomation['id']) => Promise<void>
|
|
1405
|
+
/** When set, this lane is a user-defined custom stage → show ⋮ Editar/Eliminar. */
|
|
1406
|
+
customStage?: CustomStage
|
|
1407
|
+
onEditStage?: (stage: CustomStage) => void
|
|
1408
|
+
onDeleteStage?: (stage: CustomStage) => void
|
|
1277
1409
|
children: React.ReactNode
|
|
1278
1410
|
}
|
|
1279
1411
|
|
|
@@ -1299,6 +1431,9 @@ function KanbanLane({
|
|
|
1299
1431
|
onAutomationCreate,
|
|
1300
1432
|
onAutomationUpdate,
|
|
1301
1433
|
onAutomationRemove,
|
|
1434
|
+
customStage,
|
|
1435
|
+
onEditStage,
|
|
1436
|
+
onDeleteStage,
|
|
1302
1437
|
children,
|
|
1303
1438
|
}: KanbanLaneProps) {
|
|
1304
1439
|
const { t } = useTranslation()
|
|
@@ -1409,6 +1544,13 @@ function KanbanLane({
|
|
|
1409
1544
|
onRemove={onAutomationRemove}
|
|
1410
1545
|
/>
|
|
1411
1546
|
)}
|
|
1547
|
+
{customStage && onEditStage && onDeleteStage && (
|
|
1548
|
+
<CustomStageLaneMenu
|
|
1549
|
+
stage={customStage}
|
|
1550
|
+
onEdit={onEditStage}
|
|
1551
|
+
onDelete={onDeleteStage}
|
|
1552
|
+
/>
|
|
1553
|
+
)}
|
|
1412
1554
|
</div>
|
|
1413
1555
|
</div>
|
|
1414
1556
|
{searchOpen && (
|
|
@@ -1640,6 +1782,8 @@ interface KanbanCardProps {
|
|
|
1640
1782
|
onClick?: (row: any) => void
|
|
1641
1783
|
/** STRING contract — dispatch the action by its key (see useDynamicRowActions). */
|
|
1642
1784
|
onAction: (actionKey: string, record: any) => void
|
|
1785
|
+
/** When false the card is static (no drag) — used by read-only smart lanes. */
|
|
1786
|
+
draggable?: boolean
|
|
1643
1787
|
}
|
|
1644
1788
|
|
|
1645
1789
|
function KanbanCard({
|
|
@@ -1652,19 +1796,23 @@ function KanbanCard({
|
|
|
1652
1796
|
currency,
|
|
1653
1797
|
onClick,
|
|
1654
1798
|
onAction,
|
|
1799
|
+
draggable = true,
|
|
1655
1800
|
}: KanbanCardProps) {
|
|
1656
1801
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
1657
1802
|
id: String(card.id),
|
|
1803
|
+
disabled: !draggable,
|
|
1658
1804
|
})
|
|
1659
1805
|
|
|
1660
1806
|
const visibleActions = actions.filter((a) => isRowActionVisible(a, card))
|
|
1661
1807
|
|
|
1662
1808
|
return (
|
|
1663
1809
|
<Card
|
|
1664
|
-
ref={setNodeRef}
|
|
1665
|
-
{...attributes}
|
|
1666
|
-
{...listeners}
|
|
1667
|
-
className=
|
|
1810
|
+
ref={draggable ? setNodeRef : undefined}
|
|
1811
|
+
{...(draggable ? attributes : {})}
|
|
1812
|
+
{...(draggable ? listeners : {})}
|
|
1813
|
+
className={`w-full min-w-0 border-border/70 shadow-sm ${
|
|
1814
|
+
draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'
|
|
1815
|
+
}`}
|
|
1668
1816
|
style={{ opacity: isDragging ? 0.4 : 1 }}
|
|
1669
1817
|
onClick={() => onClick?.(card)}
|
|
1670
1818
|
data-card-id={String(card.id)}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// FieldGrid — the shared, responsive form layout for the SDK's declarative
|
|
2
|
+
// modals (the automatic CRUD create/edit dialog and the placement:create /
|
|
3
|
+
// generic action modal). One source of truth so both read identically: short
|
|
4
|
+
// scalar fields (inputs, selects, dates) flow in two columns when the width
|
|
5
|
+
// allows, textareas / line-items / long-form widgets span the full row, and
|
|
6
|
+
// everything collapses to a single column on phones.
|
|
7
|
+
//
|
|
8
|
+
// The load-bearing detail is `min-w-0` on every cell: CSS grid tracks default
|
|
9
|
+
// to `minmax(auto, 1fr)`, so a child with a wide intrinsic minimum (a Select
|
|
10
|
+
// with a long option, an Input with a long value) pushes its column past the
|
|
11
|
+
// container and the whole dialog grows a horizontal scrollbar. `min-w-0` lets
|
|
12
|
+
// the cell shrink below its content and keeps the modal within its width.
|
|
13
|
+
import type { ReactNode } from 'react'
|
|
14
|
+
import { Label } from '@asteby/metacore-ui/primitives'
|
|
15
|
+
import { cn } from '@asteby/metacore-ui/lib'
|
|
16
|
+
|
|
17
|
+
export function FieldGrid({ children, className }: { children: ReactNode; className?: string }) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={cn('grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2', className)}>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FieldCell({
|
|
26
|
+
children,
|
|
27
|
+
fullWidth,
|
|
28
|
+
className,
|
|
29
|
+
}: {
|
|
30
|
+
children: ReactNode
|
|
31
|
+
/** Span both columns — textareas, line-items grids, embedded relations. */
|
|
32
|
+
fullWidth?: boolean
|
|
33
|
+
className?: string
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={cn('flex min-w-0 flex-col gap-1.5', fullWidth && 'sm:col-span-2', className)}>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FieldLabel({
|
|
43
|
+
htmlFor,
|
|
44
|
+
required,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
htmlFor?: string
|
|
48
|
+
required?: boolean
|
|
49
|
+
children: ReactNode
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<Label htmlFor={htmlFor} className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
53
|
+
{children}
|
|
54
|
+
{required && <span className="ml-0.5 text-destructive">*</span>}
|
|
55
|
+
</Label>
|
|
56
|
+
)
|
|
57
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,30 @@ export {
|
|
|
30
30
|
type UseStageAutomationsResult,
|
|
31
31
|
type StageAutomationsButtonProps,
|
|
32
32
|
} from './stage-automations'
|
|
33
|
+
export {
|
|
34
|
+
useCustomStages,
|
|
35
|
+
splitCustomStages,
|
|
36
|
+
mergeLaneStages,
|
|
37
|
+
resolveSmartLanes,
|
|
38
|
+
smartLaneParams,
|
|
39
|
+
customStageFilterFields,
|
|
40
|
+
isCustomStageDraftValid,
|
|
41
|
+
slugifyStageKey,
|
|
42
|
+
emptyCustomStageFilter,
|
|
43
|
+
AddStageColumn,
|
|
44
|
+
CustomStageLaneMenu,
|
|
45
|
+
CustomStageDialog,
|
|
46
|
+
CustomStageDeleteDialog,
|
|
47
|
+
SmartLane,
|
|
48
|
+
CUSTOM_STAGE_COLORS,
|
|
49
|
+
CUSTOM_STAGE_FILTER_OPS,
|
|
50
|
+
type CustomStage,
|
|
51
|
+
type NewCustomStage,
|
|
52
|
+
type CustomStageType,
|
|
53
|
+
type CustomStageFilter,
|
|
54
|
+
type CustomStageFilterOp,
|
|
55
|
+
type UseCustomStagesResult,
|
|
56
|
+
} from './custom-stages'
|
|
33
57
|
export {
|
|
34
58
|
DynamicView,
|
|
35
59
|
resolveViewRenderer,
|
|
@@ -43,6 +67,7 @@ export {
|
|
|
43
67
|
type UseDynamicFiltersResult,
|
|
44
68
|
} from './use-dynamic-filters'
|
|
45
69
|
export * from './dynamic-form'
|
|
70
|
+
export { FieldGrid, FieldCell, FieldLabel } from './field-grid'
|
|
46
71
|
export {
|
|
47
72
|
ActionModalDispatcher,
|
|
48
73
|
type ActionModalProps,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// it internally, and bespoke host pages (e.g. ops `/m/$model`) mount it directly
|
|
19
19
|
// next to their own toolbar. Hosts never reimplement action-button plumbing.
|
|
20
20
|
import { useEffect, useMemo, useState } from 'react'
|
|
21
|
+
import { useTranslation } from 'react-i18next'
|
|
21
22
|
import { Button } from '@asteby/metacore-ui/primitives'
|
|
22
23
|
import { useApi } from './api-context'
|
|
23
24
|
import { useMetadataCache } from './metadata-cache'
|
|
@@ -109,6 +110,7 @@ export function ModelActionToolbar({
|
|
|
109
110
|
onChange,
|
|
110
111
|
className,
|
|
111
112
|
}: ModelActionToolbarProps) {
|
|
113
|
+
const { t } = useTranslation()
|
|
112
114
|
const all = useModelActions(model, placements, actions)
|
|
113
115
|
// Capability gating — always-true without a <PermissionsProvider>. Custom
|
|
114
116
|
// table/create actions map onto `lowercase(model).<action_key>`.
|
|
@@ -135,7 +137,13 @@ export function ModelActionToolbar({
|
|
|
135
137
|
style={a.color && !isCreate ? { borderColor: a.color, color: a.color } : undefined}
|
|
136
138
|
>
|
|
137
139
|
<DynamicIcon name={a.icon || (isCreate ? 'Plus' : 'Zap')} className="mr-2 h-4 w-4" />
|
|
138
|
-
{a.label
|
|
140
|
+
{/* `a.label` is an addon-contributed i18n key (e.g.
|
|
141
|
+
"integration_github.action.create_issue.label"); the addon's
|
|
142
|
+
locale bundle loads asynchronously, so translate at render — a
|
|
143
|
+
bare `{a.label}` prints the raw key until (and after) the bundle
|
|
144
|
+
lands because nothing re-derives it. defaultValue keeps an
|
|
145
|
+
already-localized label untouched. */}
|
|
146
|
+
{t(a.label, { defaultValue: a.label })}
|
|
139
147
|
</Button>
|
|
140
148
|
)
|
|
141
149
|
})}
|
package/src/types.ts
CHANGED
|
@@ -43,6 +43,13 @@ export interface TableMetadata {
|
|
|
43
43
|
* keys as the kernel serves them.
|
|
44
44
|
*/
|
|
45
45
|
stages?: StageMeta[]
|
|
46
|
+
/**
|
|
47
|
+
* Virtual "smart" lanes (ops #704): read-only board columns defined by a set
|
|
48
|
+
* of filters rather than a stored stage value. When present the kanban paints
|
|
49
|
+
* one lane per entry (querying the list with the lane's filters) after the
|
|
50
|
+
* real stages. Purely additive — absent on hosts without custom stages.
|
|
51
|
+
*/
|
|
52
|
+
smart_lanes?: SmartLaneMeta[]
|
|
46
53
|
/**
|
|
47
54
|
* Allowed stage transitions (RFC §1.1). When present, the kanban only lets a
|
|
48
55
|
* card drop into a lane reachable from its current stage; disallowed lanes
|
|
@@ -64,6 +71,25 @@ export interface StageMeta {
|
|
|
64
71
|
color?: string
|
|
65
72
|
order?: number
|
|
66
73
|
is_final?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* True when the kernel merged a user-defined custom stage into `stages[]`
|
|
76
|
+
* (ops #704). Behaves as a normal droppable lane; the SDK just grows an
|
|
77
|
+
* Editar/Eliminar menu on it.
|
|
78
|
+
*/
|
|
79
|
+
custom?: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A virtual "smart" lane (ops #704) served in `TableMetadata.smart_lanes`. It's
|
|
84
|
+
* defined by `filters` (never a stored stage value), so the board paints it by
|
|
85
|
+
* querying the list with those conditions. Read-only — not a drop target.
|
|
86
|
+
*/
|
|
87
|
+
export interface SmartLaneMeta {
|
|
88
|
+
key: string
|
|
89
|
+
label: string
|
|
90
|
+
color?: string
|
|
91
|
+
order?: number
|
|
92
|
+
filters: { field: string; op: string; value: string }[]
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
/** Allowed `from → to` stage transition (RFC §1.1). `'*'` is a wildcard. */
|