@asteby/metacore-runtime-react 13.5.1 → 13.5.2

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 13.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - bc99aec: Gate per-row table actions by the row's `status` against the action's `requiresState`.
8
+
9
+ Row actions that declare a non-empty `requiresState` (camelCase or the snake_case
10
+ `requires_state` served by the backend) are now hidden in the row-action dropdown
11
+ unless the row's `status` value is one of the declared states. For example, an
12
+ "Iniciar trabajo" action with `requiresState: ['reception']` no longer appears on an
13
+ order already in `in_progress`.
14
+
15
+ Additive and null-safe: actions without `requiresState` (or an empty array) are always
16
+ shown, and rows without a `status` field surface every action, so there is no
17
+ regression for existing models.
18
+
3
19
  ## 13.5.1
4
20
 
5
21
  ### Patch Changes
@@ -14,6 +14,19 @@ export interface DynamicColumnsHelpers {
14
14
  */
15
15
  apiBaseUrl?: string;
16
16
  }
17
+ /**
18
+ * State-machine gate for per-row actions.
19
+ *
20
+ * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
21
+ * (snake_case, as served by some backends) is only surfaced for rows whose `status`
22
+ * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
23
+ * action (requiresState: ['reception']) on an order already in `in_progress`.
24
+ *
25
+ * Null-safe & non-regressive:
26
+ * - action without requiresState (or empty array) → always shown.
27
+ * - row with no `status` field → all actions shown.
28
+ */
29
+ export declare const isActionAllowedForRowState: (action: any, row: any) => boolean;
17
30
  /**
18
31
  * Builds the canonical column factory used by `<DynamicTable>` when the host
19
32
  * does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAqXnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAOD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAmHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAsXnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -21,6 +21,27 @@ import { DynamicIcon } from './dynamic-icon';
21
21
  import { isColumnVisibleInTable } from './column-visibility';
22
22
  const defaultGetImageUrl = (path) => path;
23
23
  const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
24
+ /**
25
+ * State-machine gate for per-row actions.
26
+ *
27
+ * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
28
+ * (snake_case, as served by some backends) is only surfaced for rows whose `status`
29
+ * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
30
+ * action (requiresState: ['reception']) on an order already in `in_progress`.
31
+ *
32
+ * Null-safe & non-regressive:
33
+ * - action without requiresState (or empty array) → always shown.
34
+ * - row with no `status` field → all actions shown.
35
+ */
36
+ export const isActionAllowedForRowState = (action, row) => {
37
+ const requires = action?.requiresState ?? action?.requires_state;
38
+ if (!Array.isArray(requires) || requires.length === 0)
39
+ return true;
40
+ const status = row?.status;
41
+ if (status === undefined || status === null || status === '')
42
+ return true;
43
+ return requires.map(String).includes(String(status));
44
+ };
24
45
  const lowerFirst = (value) => {
25
46
  if (!value)
26
47
  return value;
@@ -311,6 +332,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
311
332
  maxSize: 80,
312
333
  meta: {},
313
334
  cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: resolvedActions
335
+ .filter((action) => isActionAllowedForRowState(action, row.original))
314
336
  .filter((action) => {
315
337
  if (!action.condition)
316
338
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.5.1",
3
+ "version": "13.5.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,8 +61,8 @@
61
61
  "typescript": "^6.0.0",
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
- "@asteby/metacore-sdk": "3.1.0",
65
- "@asteby/metacore-ui": "2.1.0"
64
+ "@asteby/metacore-ui": "2.1.0",
65
+ "@asteby/metacore-sdk": "3.1.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { isActionAllowedForRowState } from '../dynamic-columns'
4
+
5
+ describe('isActionAllowedForRowState', () => {
6
+ it('hides the action when row.status is NOT in requiresState', () => {
7
+ const action = { key: 'start', requiresState: ['reception'] }
8
+ const row = { id: 1, status: 'in_progress' }
9
+ expect(isActionAllowedForRowState(action, row)).toBe(false)
10
+ })
11
+
12
+ it('shows the action when row.status IS in requiresState', () => {
13
+ const action = { key: 'start', requiresState: ['reception'] }
14
+ const row = { id: 1, status: 'reception' }
15
+ expect(isActionAllowedForRowState(action, row)).toBe(true)
16
+ })
17
+
18
+ it('shows the action when row.status matches one of several requiresState entries', () => {
19
+ const action = { key: 'finish', requiresState: ['in_progress', 'paused'] }
20
+ expect(isActionAllowedForRowState(action, { status: 'paused' })).toBe(true)
21
+ })
22
+
23
+ it('always shows the action when requiresState is empty', () => {
24
+ const action = { key: 'view', requiresState: [] as string[] }
25
+ expect(isActionAllowedForRowState(action, { status: 'whatever' })).toBe(true)
26
+ })
27
+
28
+ it('always shows the action when requiresState is absent (no regression)', () => {
29
+ const action = { key: 'view' }
30
+ expect(isActionAllowedForRowState(action, { status: 'in_progress' })).toBe(true)
31
+ })
32
+
33
+ it('shows all actions when the row has no status field (no regression)', () => {
34
+ const action = { key: 'start', requiresState: ['reception'] }
35
+ expect(isActionAllowedForRowState(action, { id: 1 })).toBe(true)
36
+ expect(isActionAllowedForRowState(action, { id: 1, status: null })).toBe(true)
37
+ expect(isActionAllowedForRowState(action, { id: 1, status: '' })).toBe(true)
38
+ })
39
+
40
+ it('tolerates the snake_case requires_state served by the backend', () => {
41
+ const action = { key: 'start', requires_state: ['reception'] }
42
+ expect(isActionAllowedForRowState(action, { status: 'reception' })).toBe(true)
43
+ expect(isActionAllowedForRowState(action, { status: 'in_progress' })).toBe(false)
44
+ })
45
+
46
+ it('coerces numeric status / state values via String() comparison', () => {
47
+ const action = { key: 'advance', requiresState: [1, 2] as unknown as string[] }
48
+ expect(isActionAllowedForRowState(action, { status: 2 })).toBe(true)
49
+ expect(isActionAllowedForRowState(action, { status: '3' })).toBe(false)
50
+ })
51
+ })
@@ -62,6 +62,26 @@ const defaultGetImageUrl = (path: string) => path
62
62
  const getNestedValue = (obj: any, path: string) =>
63
63
  path.split('.').reduce((acc, part) => acc && acc[part], obj)
64
64
 
65
+ /**
66
+ * State-machine gate for per-row actions.
67
+ *
68
+ * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
69
+ * (snake_case, as served by some backends) is only surfaced for rows whose `status`
70
+ * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
71
+ * action (requiresState: ['reception']) on an order already in `in_progress`.
72
+ *
73
+ * Null-safe & non-regressive:
74
+ * - action without requiresState (or empty array) → always shown.
75
+ * - row with no `status` field → all actions shown.
76
+ */
77
+ export const isActionAllowedForRowState = (action: any, row: any): boolean => {
78
+ const requires: unknown = action?.requiresState ?? action?.requires_state
79
+ if (!Array.isArray(requires) || requires.length === 0) return true
80
+ const status = row?.status
81
+ if (status === undefined || status === null || status === '') return true
82
+ return requires.map(String).includes(String(status))
83
+ }
84
+
65
85
  const lowerFirst = (value?: string) => {
66
86
  if (!value) return value
67
87
  return value.charAt(0).toLowerCase() + value.slice(1)
@@ -519,6 +539,7 @@ export function makeDefaultGetDynamicColumns(
519
539
  </DropdownMenuTrigger>
520
540
  <DropdownMenuContent align="end">
521
541
  {resolvedActions
542
+ .filter((action) => isActionAllowedForRowState(action, row.original))
522
543
  .filter((action) => {
523
544
  if (!action.condition) return true
524
545
  const { field, operator, value } = action.condition